From c6f6862d03620bb218a99c85431e384c5c8e5e4c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 22:02:41 +0000 Subject: [PATCH 01/11] feat: expose smooth drag mouse movement via public API --- .stats.yml | 4 ++-- src/kernel/resources/browsers/computer.py | 20 +++++++++++++++++++ .../types/browsers/computer_batch_params.py | 12 +++++++++++ .../browsers/computer_drag_mouse_params.py | 12 +++++++++++ tests/api_resources/browsers/test_computer.py | 4 ++++ 5 files changed, 50 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index ae22a711..81407436 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 103 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-bda5e58fa0bbd08761f27a1e0edbc602c44141ac9483bf6c96d52b7f4d10d9a7.yml -openapi_spec_hash: 10833b36358e8cda023e5bb0abeab0ba +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-e6936890166ce5b11abaccd511a43a8807e2abf96c1f542d4f8d94655ef27d1f.yml +openapi_spec_hash: 0146ecaea96d8136ef4a35cd04aacf22 config_hash: cff4d43372b6fa66b64e2d4150f6aa76 diff --git a/src/kernel/resources/browsers/computer.py b/src/kernel/resources/browsers/computer.py index 1357c1e8..bcde24e5 100644 --- a/src/kernel/resources/browsers/computer.py +++ b/src/kernel/resources/browsers/computer.py @@ -213,7 +213,9 @@ def drag_mouse( path: Iterable[Iterable[int]], button: Literal["left", "middle", "right"] | Omit = omit, delay: int | Omit = omit, + duration_ms: int | Omit = omit, hold_keys: SequenceNotStr[str] | Omit = omit, + smooth: bool | Omit = omit, step_delay_ms: int | Omit = omit, steps_per_segment: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -234,8 +236,14 @@ def drag_mouse( delay: Delay in milliseconds between button down and starting to move along the path. + duration_ms: Target total duration in milliseconds for the entire drag movement when + smooth=true. Omit for automatic timing based on total path length. + hold_keys: Modifier keys to hold during the drag + smooth: Use human-like Bezier curves between path waypoints instead of linear + interpolation. When true, steps_per_segment and step_delay_ms are ignored. + step_delay_ms: Delay in milliseconds between relative steps while dragging (not the initial delay). @@ -259,7 +267,9 @@ def drag_mouse( "path": path, "button": button, "delay": delay, + "duration_ms": duration_ms, "hold_keys": hold_keys, + "smooth": smooth, "step_delay_ms": step_delay_ms, "steps_per_segment": steps_per_segment, }, @@ -804,7 +814,9 @@ async def drag_mouse( path: Iterable[Iterable[int]], button: Literal["left", "middle", "right"] | Omit = omit, delay: int | Omit = omit, + duration_ms: int | Omit = omit, hold_keys: SequenceNotStr[str] | Omit = omit, + smooth: bool | Omit = omit, step_delay_ms: int | Omit = omit, steps_per_segment: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -825,8 +837,14 @@ async def drag_mouse( delay: Delay in milliseconds between button down and starting to move along the path. + duration_ms: Target total duration in milliseconds for the entire drag movement when + smooth=true. Omit for automatic timing based on total path length. + hold_keys: Modifier keys to hold during the drag + smooth: Use human-like Bezier curves between path waypoints instead of linear + interpolation. When true, steps_per_segment and step_delay_ms are ignored. + step_delay_ms: Delay in milliseconds between relative steps while dragging (not the initial delay). @@ -850,7 +868,9 @@ async def drag_mouse( "path": path, "button": button, "delay": delay, + "duration_ms": duration_ms, "hold_keys": hold_keys, + "smooth": smooth, "step_delay_ms": step_delay_ms, "steps_per_segment": steps_per_segment, }, diff --git a/src/kernel/types/browsers/computer_batch_params.py b/src/kernel/types/browsers/computer_batch_params.py index 9aca7244..db68466d 100644 --- a/src/kernel/types/browsers/computer_batch_params.py +++ b/src/kernel/types/browsers/computer_batch_params.py @@ -59,9 +59,21 @@ class ActionDragMouse(TypedDict, total=False): delay: int """Delay in milliseconds between button down and starting to move along the path.""" + duration_ms: int + """ + Target total duration in milliseconds for the entire drag movement when + smooth=true. Omit for automatic timing based on total path length. + """ + hold_keys: SequenceNotStr[str] """Modifier keys to hold during the drag""" + smooth: bool + """ + Use human-like Bezier curves between path waypoints instead of linear + interpolation. When true, steps_per_segment and step_delay_ms are ignored. + """ + step_delay_ms: int """ Delay in milliseconds between relative steps while dragging (not the initial diff --git a/src/kernel/types/browsers/computer_drag_mouse_params.py b/src/kernel/types/browsers/computer_drag_mouse_params.py index fb03b4be..c0dd4c8e 100644 --- a/src/kernel/types/browsers/computer_drag_mouse_params.py +++ b/src/kernel/types/browsers/computer_drag_mouse_params.py @@ -23,9 +23,21 @@ class ComputerDragMouseParams(TypedDict, total=False): delay: int """Delay in milliseconds between button down and starting to move along the path.""" + duration_ms: int + """ + Target total duration in milliseconds for the entire drag movement when + smooth=true. Omit for automatic timing based on total path length. + """ + hold_keys: SequenceNotStr[str] """Modifier keys to hold during the drag""" + smooth: bool + """ + Use human-like Bezier curves between path waypoints instead of linear + interpolation. When true, steps_per_segment and step_delay_ms are ignored. + """ + step_delay_ms: int """ Delay in milliseconds between relative steps while dragging (not the initial diff --git a/tests/api_resources/browsers/test_computer.py b/tests/api_resources/browsers/test_computer.py index 09960bfc..31974d5b 100644 --- a/tests/api_resources/browsers/test_computer.py +++ b/tests/api_resources/browsers/test_computer.py @@ -224,7 +224,9 @@ def test_method_drag_mouse_with_all_params(self, client: Kernel) -> None: path=[[0, 0], [0, 0]], button="left", delay=0, + duration_ms=50, hold_keys=["string"], + smooth=True, step_delay_ms=0, steps_per_segment=1, ) @@ -887,7 +889,9 @@ async def test_method_drag_mouse_with_all_params(self, async_client: AsyncKernel path=[[0, 0], [0, 0]], button="left", delay=0, + duration_ms=50, hold_keys=["string"], + smooth=True, step_delay_ms=0, steps_per_segment=1, ) From 0735b45fc92950cef3afea92a879712ea0ebdf0f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 20:04:37 +0000 Subject: [PATCH 02/11] feat: Add GPU viewport presets and GPU encoder defaults --- .stats.yml | 4 +- src/kernel/resources/browser_pools.py | 40 +++++++++++++------ src/kernel/resources/browsers/browsers.py | 20 +++++++--- src/kernel/types/browser_create_params.py | 22 +++++----- src/kernel/types/browser_create_response.py | 22 +++++----- src/kernel/types/browser_list_response.py | 22 +++++----- src/kernel/types/browser_pool.py | 22 +++++----- .../types/browser_pool_acquire_response.py | 22 +++++----- .../types/browser_pool_create_params.py | 22 +++++----- .../types/browser_pool_update_params.py | 22 +++++----- src/kernel/types/browser_retrieve_response.py | 22 +++++----- src/kernel/types/browser_update_response.py | 22 +++++----- .../invocation_list_browsers_response.py | 22 +++++----- src/kernel/types/shared/browser_viewport.py | 10 +++-- .../types/shared_params/browser_viewport.py | 10 +++-- 15 files changed, 188 insertions(+), 116 deletions(-) diff --git a/.stats.yml b/.stats.yml index 81407436..904cb96c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 103 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-e6936890166ce5b11abaccd511a43a8807e2abf96c1f542d4f8d94655ef27d1f.yml -openapi_spec_hash: 0146ecaea96d8136ef4a35cd04aacf22 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-95bb1cbe27cbed0339067fa133590e675b99cda4a9c04fad802a5b14563eb572.yml +openapi_spec_hash: 3a24e61711eedb9ea7bb7589a7df956f config_hash: cff4d43372b6fa66b64e2d4150f6aa76 diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py index a5b6e59a..ea45cd13 100644 --- a/src/kernel/resources/browser_pools.py +++ b/src/kernel/resources/browser_pools.py @@ -110,9 +110,13 @@ def create( are destroyed. Defaults to 600 seconds if not specified viewport: Initial browser window size in pixels with optional refresh rate. If omitted, - image defaults apply (1920x1080@25). Arbitrary viewport dimensions are accepted, - but the following configurations are known-good and fully tested: 2560x1440@10, - 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. + image defaults apply (1920x1080@25). For GPU images, the default is + 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended + presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, + 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. Viewports outside this list may exhibit unstable live view or recording behavior. If refresh_rate is not provided, it will be automatically determined based on the resolution (higher resolutions use lower refresh rates to keep @@ -242,9 +246,13 @@ def update( are destroyed. Defaults to 600 seconds if not specified viewport: Initial browser window size in pixels with optional refresh rate. If omitted, - image defaults apply (1920x1080@25). Arbitrary viewport dimensions are accepted, - but the following configurations are known-good and fully tested: 2560x1440@10, - 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. + image defaults apply (1920x1080@25). For GPU images, the default is + 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended + presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, + 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. Viewports outside this list may exhibit unstable live view or recording behavior. If refresh_rate is not provided, it will be automatically determined based on the resolution (higher resolutions use lower refresh rates to keep @@ -550,9 +558,13 @@ async def create( are destroyed. Defaults to 600 seconds if not specified viewport: Initial browser window size in pixels with optional refresh rate. If omitted, - image defaults apply (1920x1080@25). Arbitrary viewport dimensions are accepted, - but the following configurations are known-good and fully tested: 2560x1440@10, - 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. + image defaults apply (1920x1080@25). For GPU images, the default is + 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended + presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, + 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. Viewports outside this list may exhibit unstable live view or recording behavior. If refresh_rate is not provided, it will be automatically determined based on the resolution (higher resolutions use lower refresh rates to keep @@ -682,9 +694,13 @@ async def update( are destroyed. Defaults to 600 seconds if not specified viewport: Initial browser window size in pixels with optional refresh rate. If omitted, - image defaults apply (1920x1080@25). Arbitrary viewport dimensions are accepted, - but the following configurations are known-good and fully tested: 2560x1440@10, - 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. + image defaults apply (1920x1080@25). For GPU images, the default is + 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended + presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, + 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. Viewports outside this list may exhibit unstable live view or recording behavior. If refresh_rate is not provided, it will be automatically determined based on the resolution (higher resolutions use lower refresh rates to keep diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 235da236..e12b2842 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -196,9 +196,13 @@ def create( see is +/- 5 seconds around the specified value. viewport: Initial browser window size in pixels with optional refresh rate. If omitted, - image defaults apply (1920x1080@25). Arbitrary viewport dimensions are accepted, - but the following configurations are known-good and fully tested: 2560x1440@10, - 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. + image defaults apply (1920x1080@25). For GPU images, the default is + 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended + presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, + 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. Viewports outside this list may exhibit unstable live view or recording behavior. If refresh_rate is not provided, it will be automatically determined based on the resolution (higher resolutions use lower refresh rates to keep @@ -623,9 +627,13 @@ async def create( see is +/- 5 seconds around the specified value. viewport: Initial browser window size in pixels with optional refresh rate. If omitted, - image defaults apply (1920x1080@25). Arbitrary viewport dimensions are accepted, - but the following configurations are known-good and fully tested: 2560x1440@10, - 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. + image defaults apply (1920x1080@25). For GPU images, the default is + 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended + presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, + 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. Viewports outside this list may exhibit unstable live view or recording behavior. If refresh_rate is not provided, it will be automatically determined based on the resolution (higher resolutions use lower refresh rates to keep diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index 6df24637..fd22ee76 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -73,13 +73,17 @@ class BrowserCreateParams(TypedDict, total=False): """ viewport: BrowserViewport - """Initial browser window size in pixels with optional refresh rate. - - If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions - are accepted, but the following configurations are known-good and fully tested: - 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, - 1200x800@60. Viewports outside this list may exhibit unstable live view or - recording behavior. If refresh_rate is not provided, it will be automatically - determined based on the resolution (higher resolutions use lower refresh rates - to keep bandwidth reasonable). + """ + Initial browser window size in pixels with optional refresh rate. If omitted, + image defaults apply (1920x1080@25). For GPU images, the default is + 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended + presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, + 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. + Viewports outside this list may exhibit unstable live view or recording + behavior. If refresh_rate is not provided, it will be automatically determined + based on the resolution (higher resolutions use lower refresh rates to keep + bandwidth reasonable). """ diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index bbfd9a22..4b567659 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -66,13 +66,17 @@ class BrowserCreateResponse(BaseModel): """Session usage metrics.""" viewport: Optional[BrowserViewport] = None - """Initial browser window size in pixels with optional refresh rate. - - If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions - are accepted, but the following configurations are known-good and fully tested: - 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, - 1200x800@60. Viewports outside this list may exhibit unstable live view or - recording behavior. If refresh_rate is not provided, it will be automatically - determined based on the resolution (higher resolutions use lower refresh rates - to keep bandwidth reasonable). + """ + Initial browser window size in pixels with optional refresh rate. If omitted, + image defaults apply (1920x1080@25). For GPU images, the default is + 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended + presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, + 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. + Viewports outside this list may exhibit unstable live view or recording + behavior. If refresh_rate is not provided, it will be automatically determined + based on the resolution (higher resolutions use lower refresh rates to keep + bandwidth reasonable). """ diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index 915df11c..c6d97527 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -66,13 +66,17 @@ class BrowserListResponse(BaseModel): """Session usage metrics.""" viewport: Optional[BrowserViewport] = None - """Initial browser window size in pixels with optional refresh rate. - - If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions - are accepted, but the following configurations are known-good and fully tested: - 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, - 1200x800@60. Viewports outside this list may exhibit unstable live view or - recording behavior. If refresh_rate is not provided, it will be automatically - determined based on the resolution (higher resolutions use lower refresh rates - to keep bandwidth reasonable). + """ + Initial browser window size in pixels with optional refresh rate. If omitted, + image defaults apply (1920x1080@25). For GPU images, the default is + 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended + presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, + 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. + Viewports outside this list may exhibit unstable live view or recording + behavior. If refresh_rate is not provided, it will be automatically determined + based on the resolution (higher resolutions use lower refresh rates to keep + bandwidth reasonable). """ diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py index fc4e0f1d..c6286acc 100644 --- a/src/kernel/types/browser_pool.py +++ b/src/kernel/types/browser_pool.py @@ -68,15 +68,19 @@ class BrowserPoolConfig(BaseModel): """ viewport: Optional[BrowserViewport] = None - """Initial browser window size in pixels with optional refresh rate. - - If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions - are accepted, but the following configurations are known-good and fully tested: - 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, - 1200x800@60. Viewports outside this list may exhibit unstable live view or - recording behavior. If refresh_rate is not provided, it will be automatically - determined based on the resolution (higher resolutions use lower refresh rates - to keep bandwidth reasonable). + """ + Initial browser window size in pixels with optional refresh rate. If omitted, + image defaults apply (1920x1080@25). For GPU images, the default is + 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended + presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, + 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. + Viewports outside this list may exhibit unstable live view or recording + behavior. If refresh_rate is not provided, it will be automatically determined + based on the resolution (higher resolutions use lower refresh rates to keep + bandwidth reasonable). """ diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py index 373274c4..29cc6abc 100644 --- a/src/kernel/types/browser_pool_acquire_response.py +++ b/src/kernel/types/browser_pool_acquire_response.py @@ -66,13 +66,17 @@ class BrowserPoolAcquireResponse(BaseModel): """Session usage metrics.""" viewport: Optional[BrowserViewport] = None - """Initial browser window size in pixels with optional refresh rate. - - If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions - are accepted, but the following configurations are known-good and fully tested: - 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, - 1200x800@60. Viewports outside this list may exhibit unstable live view or - recording behavior. If refresh_rate is not provided, it will be automatically - determined based on the resolution (higher resolutions use lower refresh rates - to keep bandwidth reasonable). + """ + Initial browser window size in pixels with optional refresh rate. If omitted, + image defaults apply (1920x1080@25). For GPU images, the default is + 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended + presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, + 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. + Viewports outside this list may exhibit unstable live view or recording + behavior. If refresh_rate is not provided, it will be automatically determined + based on the resolution (higher resolutions use lower refresh rates to keep + bandwidth reasonable). """ diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py index 78268a50..63ef712b 100644 --- a/src/kernel/types/browser_pool_create_params.py +++ b/src/kernel/types/browser_pool_create_params.py @@ -67,13 +67,17 @@ class BrowserPoolCreateParams(TypedDict, total=False): """ viewport: BrowserViewport - """Initial browser window size in pixels with optional refresh rate. - - If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions - are accepted, but the following configurations are known-good and fully tested: - 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, - 1200x800@60. Viewports outside this list may exhibit unstable live view or - recording behavior. If refresh_rate is not provided, it will be automatically - determined based on the resolution (higher resolutions use lower refresh rates - to keep bandwidth reasonable). + """ + Initial browser window size in pixels with optional refresh rate. If omitted, + image defaults apply (1920x1080@25). For GPU images, the default is + 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended + presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, + 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. + Viewports outside this list may exhibit unstable live view or recording + behavior. If refresh_rate is not provided, it will be automatically determined + based on the resolution (higher resolutions use lower refresh rates to keep + bandwidth reasonable). """ diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py index 74b76a63..d1f003b5 100644 --- a/src/kernel/types/browser_pool_update_params.py +++ b/src/kernel/types/browser_pool_update_params.py @@ -73,13 +73,17 @@ class BrowserPoolUpdateParams(TypedDict, total=False): """ viewport: BrowserViewport - """Initial browser window size in pixels with optional refresh rate. - - If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions - are accepted, but the following configurations are known-good and fully tested: - 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, - 1200x800@60. Viewports outside this list may exhibit unstable live view or - recording behavior. If refresh_rate is not provided, it will be automatically - determined based on the resolution (higher resolutions use lower refresh rates - to keep bandwidth reasonable). + """ + Initial browser window size in pixels with optional refresh rate. If omitted, + image defaults apply (1920x1080@25). For GPU images, the default is + 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended + presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, + 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. + Viewports outside this list may exhibit unstable live view or recording + behavior. If refresh_rate is not provided, it will be automatically determined + based on the resolution (higher resolutions use lower refresh rates to keep + bandwidth reasonable). """ diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 76fc6ce1..9355af9a 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -66,13 +66,17 @@ class BrowserRetrieveResponse(BaseModel): """Session usage metrics.""" viewport: Optional[BrowserViewport] = None - """Initial browser window size in pixels with optional refresh rate. - - If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions - are accepted, but the following configurations are known-good and fully tested: - 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, - 1200x800@60. Viewports outside this list may exhibit unstable live view or - recording behavior. If refresh_rate is not provided, it will be automatically - determined based on the resolution (higher resolutions use lower refresh rates - to keep bandwidth reasonable). + """ + Initial browser window size in pixels with optional refresh rate. If omitted, + image defaults apply (1920x1080@25). For GPU images, the default is + 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended + presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, + 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. + Viewports outside this list may exhibit unstable live view or recording + behavior. If refresh_rate is not provided, it will be automatically determined + based on the resolution (higher resolutions use lower refresh rates to keep + bandwidth reasonable). """ diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py index fdf4fb51..3bf53e70 100644 --- a/src/kernel/types/browser_update_response.py +++ b/src/kernel/types/browser_update_response.py @@ -66,13 +66,17 @@ class BrowserUpdateResponse(BaseModel): """Session usage metrics.""" viewport: Optional[BrowserViewport] = None - """Initial browser window size in pixels with optional refresh rate. - - If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions - are accepted, but the following configurations are known-good and fully tested: - 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, - 1200x800@60. Viewports outside this list may exhibit unstable live view or - recording behavior. If refresh_rate is not provided, it will be automatically - determined based on the resolution (higher resolutions use lower refresh rates - to keep bandwidth reasonable). + """ + Initial browser window size in pixels with optional refresh rate. If omitted, + image defaults apply (1920x1080@25). For GPU images, the default is + 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended + presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, + 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. + Viewports outside this list may exhibit unstable live view or recording + behavior. If refresh_rate is not provided, it will be automatically determined + based on the resolution (higher resolutions use lower refresh rates to keep + bandwidth reasonable). """ diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py index 673615b2..0c6451ca 100644 --- a/src/kernel/types/invocation_list_browsers_response.py +++ b/src/kernel/types/invocation_list_browsers_response.py @@ -66,15 +66,19 @@ class Browser(BaseModel): """Session usage metrics.""" viewport: Optional[BrowserViewport] = None - """Initial browser window size in pixels with optional refresh rate. - - If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions - are accepted, but the following configurations are known-good and fully tested: - 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, - 1200x800@60. Viewports outside this list may exhibit unstable live view or - recording behavior. If refresh_rate is not provided, it will be automatically - determined based on the resolution (higher resolutions use lower refresh rates - to keep bandwidth reasonable). + """ + Initial browser window size in pixels with optional refresh rate. If omitted, + image defaults apply (1920x1080@25). For GPU images, the default is + 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended + presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, + 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. + Viewports outside this list may exhibit unstable live view or recording + behavior. If refresh_rate is not provided, it will be automatically determined + based on the resolution (higher resolutions use lower refresh rates to keep + bandwidth reasonable). """ diff --git a/src/kernel/types/shared/browser_viewport.py b/src/kernel/types/shared/browser_viewport.py index dacac1f2..c53505be 100644 --- a/src/kernel/types/shared/browser_viewport.py +++ b/src/kernel/types/shared/browser_viewport.py @@ -8,11 +8,15 @@ class BrowserViewport(BaseModel): - """Initial browser window size in pixels with optional refresh rate. - + """ + Initial browser window size in pixels with optional refresh rate. If omitted, image defaults apply (1920x1080@25). - Arbitrary viewport dimensions are accepted, but the following configurations are known-good and fully tested: + For GPU images, the default is 1920x1080@60. + Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. + For GPU images, recommended presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. Viewports outside this list may exhibit unstable live view or recording behavior. If refresh_rate is not provided, it will be automatically determined based on the resolution (higher resolutions use lower refresh rates to keep bandwidth reasonable). diff --git a/src/kernel/types/shared_params/browser_viewport.py b/src/kernel/types/shared_params/browser_viewport.py index f98ece82..2290f930 100644 --- a/src/kernel/types/shared_params/browser_viewport.py +++ b/src/kernel/types/shared_params/browser_viewport.py @@ -8,11 +8,15 @@ class BrowserViewport(TypedDict, total=False): - """Initial browser window size in pixels with optional refresh rate. - + """ + Initial browser window size in pixels with optional refresh rate. If omitted, image defaults apply (1920x1080@25). - Arbitrary viewport dimensions are accepted, but the following configurations are known-good and fully tested: + For GPU images, the default is 1920x1080@60. + Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. + For GPU images, recommended presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. Viewports outside this list may exhibit unstable live view or recording behavior. If refresh_rate is not provided, it will be automatically determined based on the resolution (higher resolutions use lower refresh rates to keep bandwidth reasonable). From 9841aac21588beb0c6a22baf5c5b0bf8e8cdd024 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 13 Mar 2026 13:26:07 +0000 Subject: [PATCH 03/11] feat: Adds description to OAS spec for docs about delta_x, delta_y --- .stats.yml | 4 ++-- src/kernel/resources/browsers/computer.py | 12 ++++++++---- src/kernel/types/browsers/computer_batch_params.py | 10 ++++++++-- src/kernel/types/browsers/computer_scroll_params.py | 10 ++++++++-- 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/.stats.yml b/.stats.yml index 904cb96c..c129c7d3 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 103 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-95bb1cbe27cbed0339067fa133590e675b99cda4a9c04fad802a5b14563eb572.yml -openapi_spec_hash: 3a24e61711eedb9ea7bb7589a7df956f +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-aa981bcc44bf8382844c53b705f75eeac53fdc7cd828a9260755c5b4537ed966.yml +openapi_spec_hash: e78521a8956dc87b25c076e30600a95e config_hash: cff4d43372b6fa66b64e2d4150f6aa76 diff --git a/src/kernel/resources/browsers/computer.py b/src/kernel/resources/browsers/computer.py index bcde24e5..116a7037 100644 --- a/src/kernel/resources/browsers/computer.py +++ b/src/kernel/resources/browsers/computer.py @@ -486,9 +486,11 @@ def scroll( y: Y coordinate at which to perform the scroll - delta_x: Horizontal scroll amount. Positive scrolls right, negative scrolls left. + delta_x: Horizontal scroll amount in xdotool "wheel units." Positive scrolls right, + negative scrolls left. - delta_y: Vertical scroll amount. Positive scrolls down, negative scrolls up. + delta_y: Vertical scroll amount in xdotool "wheel units." Positive scrolls down, negative + scrolls up. hold_keys: Modifier keys to hold during the scroll @@ -1087,9 +1089,11 @@ async def scroll( y: Y coordinate at which to perform the scroll - delta_x: Horizontal scroll amount. Positive scrolls right, negative scrolls left. + delta_x: Horizontal scroll amount in xdotool "wheel units." Positive scrolls right, + negative scrolls left. - delta_y: Vertical scroll amount. Positive scrolls down, negative scrolls up. + delta_y: Vertical scroll amount in xdotool "wheel units." Positive scrolls down, negative + scrolls up. hold_keys: Modifier keys to hold during the scroll diff --git a/src/kernel/types/browsers/computer_batch_params.py b/src/kernel/types/browsers/computer_batch_params.py index db68466d..7fc6abb5 100644 --- a/src/kernel/types/browsers/computer_batch_params.py +++ b/src/kernel/types/browsers/computer_batch_params.py @@ -131,10 +131,16 @@ class ActionScroll(TypedDict, total=False): """Y coordinate at which to perform the scroll""" delta_x: int - """Horizontal scroll amount. Positive scrolls right, negative scrolls left.""" + """ + Horizontal scroll amount in xdotool "wheel units." Positive scrolls right, + negative scrolls left. + """ delta_y: int - """Vertical scroll amount. Positive scrolls down, negative scrolls up.""" + """ + Vertical scroll amount in xdotool "wheel units." Positive scrolls down, negative + scrolls up. + """ hold_keys: SequenceNotStr[str] """Modifier keys to hold during the scroll""" diff --git a/src/kernel/types/browsers/computer_scroll_params.py b/src/kernel/types/browsers/computer_scroll_params.py index 110cb302..3af38af3 100644 --- a/src/kernel/types/browsers/computer_scroll_params.py +++ b/src/kernel/types/browsers/computer_scroll_params.py @@ -17,10 +17,16 @@ class ComputerScrollParams(TypedDict, total=False): """Y coordinate at which to perform the scroll""" delta_x: int - """Horizontal scroll amount. Positive scrolls right, negative scrolls left.""" + """ + Horizontal scroll amount in xdotool "wheel units." Positive scrolls right, + negative scrolls left. + """ delta_y: int - """Vertical scroll amount. Positive scrolls down, negative scrolls up.""" + """ + Vertical scroll amount in xdotool "wheel units." Positive scrolls down, negative + scrolls up. + """ hold_keys: SequenceNotStr[str] """Modifier keys to hold during the scroll""" From 9ee4d0c080da7f649a3bde63640593f33d5d0f6b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 22:33:43 +0000 Subject: [PATCH 04/11] feat: Rename hardware acceleration UI/docs wording to GPU acceleration --- .stats.yml | 4 ++-- src/kernel/resources/browsers/browsers.py | 8 ++++---- src/kernel/types/browser_create_params.py | 2 +- 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_response.py | 2 +- src/kernel/types/invocation_list_browsers_response.py | 2 +- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.stats.yml b/.stats.yml index c129c7d3..663d1ab4 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 103 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-aa981bcc44bf8382844c53b705f75eeac53fdc7cd828a9260755c5b4537ed966.yml -openapi_spec_hash: e78521a8956dc87b25c076e30600a95e +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-f57c1468805aef5055a41e942a1ec374df98d58f1071b07c31e6496045e0d902.yml +openapi_spec_hash: a4848d54211d6c6330b5ddd08992035a config_hash: cff4d43372b6fa66b64e2d4150f6aa76 diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index e12b2842..a112bdaa 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -166,8 +166,8 @@ def create( Args: extensions: List of browser extensions to load into the session. Provide each by id or name. - gpu: If true, launches a hardware-accelerated browser with GPU rendering. Requires - Start-Up or Enterprise plan. + gpu: If true, enables GPU acceleration for the browser session. Requires Start-Up or + Enterprise plan. headless: If true, launches the browser using a headless image (no VNC/GUI). Defaults to false. @@ -597,8 +597,8 @@ async def create( Args: extensions: List of browser extensions to load into the session. Provide each by id or name. - gpu: If true, launches a hardware-accelerated browser with GPU rendering. Requires - Start-Up or Enterprise plan. + gpu: If true, enables GPU acceleration for the browser session. Requires Start-Up or + Enterprise plan. headless: If true, launches the browser using a headless image (no VNC/GUI). Defaults to false. diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index fd22ee76..f59959e0 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -21,7 +21,7 @@ class BrowserCreateParams(TypedDict, total=False): """ gpu: bool - """If true, launches a hardware-accelerated browser with GPU rendering. + """If true, enables GPU acceleration for the browser session. Requires Start-Up or Enterprise plan. """ diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index 4b567659..f1f8c781 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -45,7 +45,7 @@ class BrowserCreateResponse(BaseModel): """When the browser session was soft-deleted. Only present for deleted sessions.""" gpu: Optional[bool] = None - """Whether the browser session has hardware-accelerated GPU rendering.""" + """Whether GPU acceleration is enabled for the browser session.""" kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index c6d97527..8dbaf1dd 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -45,7 +45,7 @@ class BrowserListResponse(BaseModel): """When the browser session was soft-deleted. Only present for deleted sessions.""" gpu: Optional[bool] = None - """Whether the browser session has hardware-accelerated GPU rendering.""" + """Whether GPU acceleration is enabled for the browser session.""" kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py index 29cc6abc..24e436ef 100644 --- a/src/kernel/types/browser_pool_acquire_response.py +++ b/src/kernel/types/browser_pool_acquire_response.py @@ -45,7 +45,7 @@ class BrowserPoolAcquireResponse(BaseModel): """When the browser session was soft-deleted. Only present for deleted sessions.""" gpu: Optional[bool] = None - """Whether the browser session has hardware-accelerated GPU rendering.""" + """Whether GPU acceleration is enabled for the browser session.""" kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 9355af9a..8b8353f5 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -45,7 +45,7 @@ class BrowserRetrieveResponse(BaseModel): """When the browser session was soft-deleted. Only present for deleted sessions.""" gpu: Optional[bool] = None - """Whether the browser session has hardware-accelerated GPU rendering.""" + """Whether GPU acceleration is enabled for the browser session.""" kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py index 3bf53e70..5b90fa43 100644 --- a/src/kernel/types/browser_update_response.py +++ b/src/kernel/types/browser_update_response.py @@ -45,7 +45,7 @@ class BrowserUpdateResponse(BaseModel): """When the browser session was soft-deleted. Only present for deleted sessions.""" gpu: Optional[bool] = None - """Whether the browser session has hardware-accelerated GPU rendering.""" + """Whether GPU acceleration is enabled for the browser session.""" kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py index 0c6451ca..7e4225ec 100644 --- a/src/kernel/types/invocation_list_browsers_response.py +++ b/src/kernel/types/invocation_list_browsers_response.py @@ -45,7 +45,7 @@ class Browser(BaseModel): """When the browser session was soft-deleted. Only present for deleted sessions.""" gpu: Optional[bool] = None - """Whether the browser session has hardware-accelerated GPU rendering.""" + """Whether GPU acceleration is enabled for the browser session.""" kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" From a815a8237cce2098b2f5d6a8ad5def400d418fbb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 04:58:44 +0000 Subject: [PATCH 05/11] fix(pydantic): do not pass `by_alias` unless set --- src/kernel/_compat.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/kernel/_compat.py b/src/kernel/_compat.py index 786ff42a..e6690a4f 100644 --- a/src/kernel/_compat.py +++ b/src/kernel/_compat.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload from datetime import date, datetime -from typing_extensions import Self, Literal +from typing_extensions import Self, Literal, TypedDict import pydantic from pydantic.fields import FieldInfo @@ -131,6 +131,10 @@ def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str: return model.model_dump_json(indent=indent) +class _ModelDumpKwargs(TypedDict, total=False): + by_alias: bool + + def model_dump( model: pydantic.BaseModel, *, @@ -142,6 +146,9 @@ def model_dump( by_alias: bool | None = None, ) -> dict[str, Any]: if (not PYDANTIC_V1) or hasattr(model, "model_dump"): + kwargs: _ModelDumpKwargs = {} + if by_alias is not None: + kwargs["by_alias"] = by_alias return model.model_dump( mode=mode, exclude=exclude, @@ -149,7 +156,7 @@ def model_dump( exclude_defaults=exclude_defaults, # warnings are not supported in Pydantic v1 warnings=True if PYDANTIC_V1 else warnings, - by_alias=by_alias, + **kwargs, ) return cast( "dict[str, Any]", From fd55947f1776b43cca804622d0c771ebe99ead60 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 05:03:36 +0000 Subject: [PATCH 06/11] fix(deps): bump minimum typing-extensions version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 021d31eb..62a14748 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ authors = [ dependencies = [ "httpx>=0.23.0, <1", "pydantic>=1.9.0, <3", - "typing-extensions>=4.10, <5", + "typing-extensions>=4.14, <5", "anyio>=3.5.0, <5", "distro>=1.7.0, <2", "sniffio", From 8781c7d9aa8759e487e5d86cbb82bfb7eb3d314e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 05:06:36 +0000 Subject: [PATCH 07/11] chore(internal): tweak CI branches --- .github/workflows/ci.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f78a6a1..c7bc99ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,14 @@ name: CI on: push: - branches-ignore: - - 'generated' - - 'codegen/**' - - 'integrated/**' - - 'stl-preview-head/**' - - 'stl-preview-base/**' + branches: + - '**' + - '!integrated/**' + - '!stl-preview-head/**' + - '!stl-preview-base/**' + - '!generated' + - '!codegen/**' + - 'codegen/stl/**' pull_request: branches-ignore: - 'stl-preview-head/**' From cda8f94f6cebabc3b3b6f95aff765816255a9270 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:14:54 +0000 Subject: [PATCH 08/11] feat: Drop headless GPU support and disable pooling --- .stats.yml | 4 ++-- src/kernel/resources/browsers/browsers.py | 4 ++-- src/kernel/types/browser_create_params.py | 2 +- src/kernel/types/browser_create_response.py | 5 ++++- src/kernel/types/browser_list_response.py | 5 ++++- src/kernel/types/browser_pool_acquire_response.py | 5 ++++- src/kernel/types/browser_retrieve_response.py | 5 ++++- src/kernel/types/browser_update_response.py | 5 ++++- src/kernel/types/invocation_list_browsers_response.py | 5 ++++- 9 files changed, 29 insertions(+), 11 deletions(-) diff --git a/.stats.yml b/.stats.yml index 663d1ab4..ad84265a 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 103 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-f57c1468805aef5055a41e942a1ec374df98d58f1071b07c31e6496045e0d902.yml -openapi_spec_hash: a4848d54211d6c6330b5ddd08992035a +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-17e50cf93d8052ff655c160fc0f156621d9029b041526d4e2e3317b13f80822f.yml +openapi_spec_hash: f7dadc8d93e77983936eb18a8080ce15 config_hash: cff4d43372b6fa66b64e2d4150f6aa76 diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index a112bdaa..1d1ce22a 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -167,7 +167,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. + Enterprise plan and headless=false. headless: If true, launches the browser using a headless image (no VNC/GUI). Defaults to false. @@ -598,7 +598,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. + Enterprise plan and headless=false. headless: If true, launches the browser using a headless image (no VNC/GUI). Defaults to false. diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index f59959e0..2827b1dc 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -23,7 +23,7 @@ class BrowserCreateParams(TypedDict, total=False): gpu: bool """If true, enables GPU acceleration for the browser session. - Requires Start-Up or Enterprise plan. + Requires Start-Up or Enterprise plan and headless=false. """ headless: bool diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index f1f8c781..d59a3d0d 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -45,7 +45,10 @@ class BrowserCreateResponse(BaseModel): """When the browser session was soft-deleted. Only present for deleted sessions.""" gpu: Optional[bool] = None - """Whether GPU acceleration is enabled for the browser session.""" + """ + Whether GPU acceleration is enabled for the browser session (only supported for + headful sessions). + """ kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index 8dbaf1dd..708caa97 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -45,7 +45,10 @@ class BrowserListResponse(BaseModel): """When the browser session was soft-deleted. Only present for deleted sessions.""" gpu: Optional[bool] = None - """Whether GPU acceleration is enabled for the browser session.""" + """ + Whether GPU acceleration is enabled for the browser session (only supported for + headful sessions). + """ kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py index 24e436ef..5ab52b58 100644 --- a/src/kernel/types/browser_pool_acquire_response.py +++ b/src/kernel/types/browser_pool_acquire_response.py @@ -45,7 +45,10 @@ class BrowserPoolAcquireResponse(BaseModel): """When the browser session was soft-deleted. Only present for deleted sessions.""" gpu: Optional[bool] = None - """Whether GPU acceleration is enabled for the browser session.""" + """ + Whether GPU acceleration is enabled for the browser session (only supported for + headful sessions). + """ kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 8b8353f5..221eab52 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -45,7 +45,10 @@ class BrowserRetrieveResponse(BaseModel): """When the browser session was soft-deleted. Only present for deleted sessions.""" gpu: Optional[bool] = None - """Whether GPU acceleration is enabled for the browser session.""" + """ + Whether GPU acceleration is enabled for the browser session (only supported for + headful sessions). + """ kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py index 5b90fa43..c8a85c3b 100644 --- a/src/kernel/types/browser_update_response.py +++ b/src/kernel/types/browser_update_response.py @@ -45,7 +45,10 @@ class BrowserUpdateResponse(BaseModel): """When the browser session was soft-deleted. Only present for deleted sessions.""" gpu: Optional[bool] = None - """Whether GPU acceleration is enabled for the browser session.""" + """ + Whether GPU acceleration is enabled for the browser session (only supported for + headful sessions). + """ kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py index 7e4225ec..a0fed9a2 100644 --- a/src/kernel/types/invocation_list_browsers_response.py +++ b/src/kernel/types/invocation_list_browsers_response.py @@ -45,7 +45,10 @@ class Browser(BaseModel): """When the browser session was soft-deleted. Only present for deleted sessions.""" gpu: Optional[bool] = None - """Whether GPU acceleration is enabled for the browser session.""" + """ + Whether GPU acceleration is enabled for the browser session (only supported for + headful sessions). + """ kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" From 9b55d2be9472f779fe20d7d863dacae1030d2b49 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 02:28:01 +0000 Subject: [PATCH 09/11] fix: sanitize endpoint path params --- src/kernel/_utils/__init__.py | 1 + src/kernel/_utils/_path.py | 127 +++++++++++++++++++ src/kernel/resources/auth/connections.py | 22 ++-- src/kernel/resources/browser_pools.py | 26 ++-- src/kernel/resources/browsers/browsers.py | 18 +-- src/kernel/resources/browsers/computer.py | 50 ++++---- src/kernel/resources/browsers/fs/fs.py | 50 ++++---- src/kernel/resources/browsers/fs/watch.py | 14 +- src/kernel/resources/browsers/logs.py | 6 +- src/kernel/resources/browsers/playwright.py | 6 +- src/kernel/resources/browsers/process.py | 30 ++--- src/kernel/resources/browsers/replays.py | 18 +-- src/kernel/resources/credential_providers.py | 22 ++-- src/kernel/resources/credentials.py | 18 +-- src/kernel/resources/deployments.py | 14 +- src/kernel/resources/extensions.py | 10 +- src/kernel/resources/invocations.py | 22 ++-- src/kernel/resources/profiles.py | 14 +- src/kernel/resources/proxies.py | 14 +- tests/test_utils/test_path.py | 89 +++++++++++++ 20 files changed, 394 insertions(+), 177 deletions(-) create mode 100644 src/kernel/_utils/_path.py create mode 100644 tests/test_utils/test_path.py diff --git a/src/kernel/_utils/__init__.py b/src/kernel/_utils/__init__.py index dc64e29a..10cb66d2 100644 --- a/src/kernel/_utils/__init__.py +++ b/src/kernel/_utils/__init__.py @@ -1,3 +1,4 @@ +from ._path import path_template as path_template from ._sync import asyncify as asyncify from ._proxy import LazyProxy as LazyProxy from ._utils import ( diff --git a/src/kernel/_utils/_path.py b/src/kernel/_utils/_path.py new file mode 100644 index 00000000..4d6e1e4c --- /dev/null +++ b/src/kernel/_utils/_path.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +import re +from typing import ( + Any, + Mapping, + Callable, +) +from urllib.parse import quote + +# Matches '.' or '..' where each dot is either literal or percent-encoded (%2e / %2E). +_DOT_SEGMENT_RE = re.compile(r"^(?:\.|%2[eE]){1,2}$") + +_PLACEHOLDER_RE = re.compile(r"\{(\w+)\}") + + +def _quote_path_segment_part(value: str) -> str: + """Percent-encode `value` for use in a URI path segment. + + Considers characters not in `pchar` set from RFC 3986 §3.3 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.3 + """ + # quote() already treats unreserved characters (letters, digits, and -._~) + # as safe, so we only need to add sub-delims, ':', and '@'. + # Notably, unlike the default `safe` for quote(), / is unsafe and must be quoted. + return quote(value, safe="!$&'()*+,;=:@") + + +def _quote_query_part(value: str) -> str: + """Percent-encode `value` for use in a URI query string. + + Considers &, = and characters not in `query` set from RFC 3986 §3.4 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.4 + """ + return quote(value, safe="!$'()*+,;:@/?") + + +def _quote_fragment_part(value: str) -> str: + """Percent-encode `value` for use in a URI fragment. + + Considers characters not in `fragment` set from RFC 3986 §3.5 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.5 + """ + return quote(value, safe="!$&'()*+,;=:@/?") + + +def _interpolate( + template: str, + values: Mapping[str, Any], + quoter: Callable[[str], str], +) -> str: + """Replace {name} placeholders in `template`, quoting each value with `quoter`. + + Placeholder names are looked up in `values`. + + Raises: + KeyError: If a placeholder is not found in `values`. + """ + # re.split with a capturing group returns alternating + # [text, name, text, name, ..., text] elements. + parts = _PLACEHOLDER_RE.split(template) + + for i in range(1, len(parts), 2): + name = parts[i] + if name not in values: + raise KeyError(f"a value for placeholder {{{name}}} was not provided") + val = values[name] + if val is None: + parts[i] = "null" + elif isinstance(val, bool): + parts[i] = "true" if val else "false" + else: + parts[i] = quoter(str(values[name])) + + return "".join(parts) + + +def path_template(template: str, /, **kwargs: Any) -> str: + """Interpolate {name} placeholders in `template` from keyword arguments. + + Args: + template: The template string containing {name} placeholders. + **kwargs: Keyword arguments to interpolate into the template. + + Returns: + The template with placeholders interpolated and percent-encoded. + + Safe characters for percent-encoding are dependent on the URI component. + Placeholders in path and fragment portions are percent-encoded where the `segment` + and `fragment` sets from RFC 3986 respectively are considered safe. + Placeholders in the query portion are percent-encoded where the `query` set from + RFC 3986 §3.3 is considered safe except for = and & characters. + + Raises: + KeyError: If a placeholder is not found in `kwargs`. + ValueError: If resulting path contains /./ or /../ segments (including percent-encoded dot-segments). + """ + # Split the template into path, query, and fragment portions. + fragment_template: str | None = None + query_template: str | None = None + + rest = template + if "#" in rest: + rest, fragment_template = rest.split("#", 1) + if "?" in rest: + rest, query_template = rest.split("?", 1) + path_template = rest + + # Interpolate each portion with the appropriate quoting rules. + path_result = _interpolate(path_template, kwargs, _quote_path_segment_part) + + # Reject dot-segments (. and ..) in the final assembled path. The check + # runs after interpolation so that adjacent placeholders or a mix of static + # text and placeholders that together form a dot-segment are caught. + # Also reject percent-encoded dot-segments to protect against incorrectly + # implemented normalization in servers/proxies. + for segment in path_result.split("/"): + if _DOT_SEGMENT_RE.match(segment): + raise ValueError(f"Constructed path {path_result!r} contains dot-segment {segment!r} which is not allowed") + + result = path_result + if query_template is not None: + result += "?" + _interpolate(query_template, kwargs, _quote_query_part) + if fragment_template is not None: + result += "#" + _interpolate(fragment_template, kwargs, _quote_fragment_part) + + return result diff --git a/src/kernel/resources/auth/connections.py b/src/kernel/resources/auth/connections.py index fed66797..0915365b 100644 --- a/src/kernel/resources/auth/connections.py +++ b/src/kernel/resources/auth/connections.py @@ -7,7 +7,7 @@ import httpx from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, SequenceNotStr, omit, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -179,7 +179,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/auth/connections/{id}", + path_template("/auth/connections/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -273,7 +273,7 @@ def delete( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( - f"/auth/connections/{id}", + path_template("/auth/connections/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -309,7 +309,7 @@ def follow( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} return self._get( - f"/auth/connections/{id}/events", + path_template("/auth/connections/{id}/events", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -353,7 +353,7 @@ def login( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/auth/connections/{id}/login", + path_template("/auth/connections/{id}/login", id=id), body=maybe_transform({"proxy": proxy}, connection_login_params.ConnectionLoginParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -398,7 +398,7 @@ def submit( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/auth/connections/{id}/submit", + path_template("/auth/connections/{id}/submit", id=id), body=maybe_transform( { "fields": fields, @@ -560,7 +560,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/auth/connections/{id}", + path_template("/auth/connections/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -654,7 +654,7 @@ async def delete( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( - f"/auth/connections/{id}", + path_template("/auth/connections/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -690,7 +690,7 @@ async def follow( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} return await self._get( - f"/auth/connections/{id}/events", + path_template("/auth/connections/{id}/events", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -734,7 +734,7 @@ async def login( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/auth/connections/{id}/login", + path_template("/auth/connections/{id}/login", id=id), body=await async_maybe_transform({"proxy": proxy}, connection_login_params.ConnectionLoginParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -779,7 +779,7 @@ async def submit( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/auth/connections/{id}/submit", + path_template("/auth/connections/{id}/submit", id=id), body=await async_maybe_transform( { "fields": fields, diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py index ea45cd13..c0ce6595 100644 --- a/src/kernel/resources/browser_pools.py +++ b/src/kernel/resources/browser_pools.py @@ -14,7 +14,7 @@ browser_pool_release_params, ) from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -180,7 +180,7 @@ def retrieve( if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return self._get( - f"/browser_pools/{id_or_name}", + path_template("/browser_pools/{id_or_name}", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -269,7 +269,7 @@ def update( if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return self._patch( - f"/browser_pools/{id_or_name}", + path_template("/browser_pools/{id_or_name}", id_or_name=id_or_name), body=maybe_transform( { "size": size, @@ -345,7 +345,7 @@ def delete( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( - f"/browser_pools/{id_or_name}", + path_template("/browser_pools/{id_or_name}", id_or_name=id_or_name), body=maybe_transform({"force": force}, browser_pool_delete_params.BrowserPoolDeleteParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -388,7 +388,7 @@ def acquire( if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return self._post( - f"/browser_pools/{id_or_name}/acquire", + path_template("/browser_pools/{id_or_name}/acquire", id_or_name=id_or_name), body=maybe_transform( {"acquire_timeout_seconds": acquire_timeout_seconds}, browser_pool_acquire_params.BrowserPoolAcquireParams, @@ -426,7 +426,7 @@ def flush( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._post( - f"/browser_pools/{id_or_name}/flush", + path_template("/browser_pools/{id_or_name}/flush", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -467,7 +467,7 @@ def release( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._post( - f"/browser_pools/{id_or_name}/release", + path_template("/browser_pools/{id_or_name}/release", id_or_name=id_or_name), body=maybe_transform( { "session_id": session_id, @@ -628,7 +628,7 @@ async def retrieve( if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return await self._get( - f"/browser_pools/{id_or_name}", + path_template("/browser_pools/{id_or_name}", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -717,7 +717,7 @@ async def update( if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return await self._patch( - f"/browser_pools/{id_or_name}", + path_template("/browser_pools/{id_or_name}", id_or_name=id_or_name), body=await async_maybe_transform( { "size": size, @@ -793,7 +793,7 @@ async def delete( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( - f"/browser_pools/{id_or_name}", + path_template("/browser_pools/{id_or_name}", id_or_name=id_or_name), body=await async_maybe_transform({"force": force}, browser_pool_delete_params.BrowserPoolDeleteParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -836,7 +836,7 @@ async def acquire( if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return await self._post( - f"/browser_pools/{id_or_name}/acquire", + path_template("/browser_pools/{id_or_name}/acquire", id_or_name=id_or_name), body=await async_maybe_transform( {"acquire_timeout_seconds": acquire_timeout_seconds}, browser_pool_acquire_params.BrowserPoolAcquireParams, @@ -874,7 +874,7 @@ async def flush( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._post( - f"/browser_pools/{id_or_name}/flush", + path_template("/browser_pools/{id_or_name}/flush", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -915,7 +915,7 @@ async def release( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._post( - f"/browser_pools/{id_or_name}/release", + path_template("/browser_pools/{id_or_name}/release", id_or_name=id_or_name), body=await async_maybe_transform( { "session_id": session_id, diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 1d1ce22a..c28a16c7 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -49,7 +49,7 @@ AsyncReplaysResourceWithStreamingResponse, ) from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given -from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform +from ..._utils import extract_files, path_template, maybe_transform, deepcopy_minimal, async_maybe_transform from .computer import ( ComputerResource, AsyncComputerResource, @@ -269,7 +269,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/browsers/{id}", + path_template("/browsers/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -319,7 +319,7 @@ def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._patch( - f"/browsers/{id}", + path_template("/browsers/{id}", id=id), body=maybe_transform( { "profile": profile, @@ -465,7 +465,7 @@ def delete_by_id( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( - f"/browsers/{id}", + path_template("/browsers/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -509,7 +509,7 @@ def load_extensions( # multipart/form-data; boundary=---abc-- extra_headers["Content-Type"] = "multipart/form-data" return self._post( - f"/browsers/{id}/extensions", + path_template("/browsers/{id}/extensions", id=id), body=maybe_transform(body, browser_load_extensions_params.BrowserLoadExtensionsParams), files=files, options=make_request_options( @@ -700,7 +700,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/browsers/{id}", + path_template("/browsers/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -750,7 +750,7 @@ async def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._patch( - f"/browsers/{id}", + path_template("/browsers/{id}", id=id), body=await async_maybe_transform( { "profile": profile, @@ -898,7 +898,7 @@ async def delete_by_id( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( - f"/browsers/{id}", + path_template("/browsers/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -942,7 +942,7 @@ async def load_extensions( # multipart/form-data; boundary=---abc-- extra_headers["Content-Type"] = "multipart/form-data" return await self._post( - f"/browsers/{id}/extensions", + path_template("/browsers/{id}/extensions", id=id), body=await async_maybe_transform(body, browser_load_extensions_params.BrowserLoadExtensionsParams), files=files, options=make_request_options( diff --git a/src/kernel/resources/browsers/computer.py b/src/kernel/resources/browsers/computer.py index 116a7037..54b638e5 100644 --- a/src/kernel/resources/browsers/computer.py +++ b/src/kernel/resources/browsers/computer.py @@ -8,7 +8,7 @@ import httpx from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, SequenceNotStr, omit, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -97,7 +97,7 @@ def batch( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._post( - f"/browsers/{id}/computer/batch", + path_template("/browsers/{id}/computer/batch", id=id), body=maybe_transform({"actions": actions}, computer_batch_params.ComputerBatchParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -133,7 +133,7 @@ def capture_screenshot( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "image/png", **(extra_headers or {})} return self._post( - f"/browsers/{id}/computer/screenshot", + path_template("/browsers/{id}/computer/screenshot", id=id), body=maybe_transform( {"region": region}, computer_capture_screenshot_params.ComputerCaptureScreenshotParams ), @@ -188,7 +188,7 @@ def click_mouse( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._post( - f"/browsers/{id}/computer/click_mouse", + path_template("/browsers/{id}/computer/click_mouse", id=id), body=maybe_transform( { "x": x, @@ -261,7 +261,7 @@ def drag_mouse( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._post( - f"/browsers/{id}/computer/drag_mouse", + path_template("/browsers/{id}/computer/drag_mouse", id=id), body=maybe_transform( { "path": path, @@ -307,7 +307,7 @@ def get_mouse_position( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/browsers/{id}/computer/get_mouse_position", + path_template("/browsers/{id}/computer/get_mouse_position", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -357,7 +357,7 @@ def move_mouse( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._post( - f"/browsers/{id}/computer/move_mouse", + path_template("/browsers/{id}/computer/move_mouse", id=id), body=maybe_transform( { "x": x, @@ -414,7 +414,7 @@ def press_key( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._post( - f"/browsers/{id}/computer/press_key", + path_template("/browsers/{id}/computer/press_key", id=id), body=maybe_transform( { "keys": keys, @@ -455,7 +455,7 @@ def read_clipboard( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/browsers/{id}/computer/clipboard/read", + path_template("/browsers/{id}/computer/clipboard/read", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -506,7 +506,7 @@ def scroll( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._post( - f"/browsers/{id}/computer/scroll", + path_template("/browsers/{id}/computer/scroll", id=id), body=maybe_transform( { "x": x, @@ -552,7 +552,7 @@ def set_cursor_visibility( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/browsers/{id}/computer/cursor", + path_template("/browsers/{id}/computer/cursor", id=id), body=maybe_transform( {"hidden": hidden}, computer_set_cursor_visibility_params.ComputerSetCursorVisibilityParams ), @@ -595,7 +595,7 @@ def type_text( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._post( - f"/browsers/{id}/computer/type", + path_template("/browsers/{id}/computer/type", id=id), body=maybe_transform( { "text": text, @@ -639,7 +639,7 @@ def write_clipboard( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._post( - f"/browsers/{id}/computer/clipboard/write", + path_template("/browsers/{id}/computer/clipboard/write", id=id), body=maybe_transform({"text": text}, computer_write_clipboard_params.ComputerWriteClipboardParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -700,7 +700,7 @@ async def batch( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._post( - f"/browsers/{id}/computer/batch", + path_template("/browsers/{id}/computer/batch", id=id), body=await async_maybe_transform({"actions": actions}, computer_batch_params.ComputerBatchParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -736,7 +736,7 @@ async def capture_screenshot( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "image/png", **(extra_headers or {})} return await self._post( - f"/browsers/{id}/computer/screenshot", + path_template("/browsers/{id}/computer/screenshot", id=id), body=await async_maybe_transform( {"region": region}, computer_capture_screenshot_params.ComputerCaptureScreenshotParams ), @@ -791,7 +791,7 @@ async def click_mouse( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._post( - f"/browsers/{id}/computer/click_mouse", + path_template("/browsers/{id}/computer/click_mouse", id=id), body=await async_maybe_transform( { "x": x, @@ -864,7 +864,7 @@ async def drag_mouse( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._post( - f"/browsers/{id}/computer/drag_mouse", + path_template("/browsers/{id}/computer/drag_mouse", id=id), body=await async_maybe_transform( { "path": path, @@ -910,7 +910,7 @@ async def get_mouse_position( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/browsers/{id}/computer/get_mouse_position", + path_template("/browsers/{id}/computer/get_mouse_position", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -960,7 +960,7 @@ async def move_mouse( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._post( - f"/browsers/{id}/computer/move_mouse", + path_template("/browsers/{id}/computer/move_mouse", id=id), body=await async_maybe_transform( { "x": x, @@ -1017,7 +1017,7 @@ async def press_key( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._post( - f"/browsers/{id}/computer/press_key", + path_template("/browsers/{id}/computer/press_key", id=id), body=await async_maybe_transform( { "keys": keys, @@ -1058,7 +1058,7 @@ async def read_clipboard( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/browsers/{id}/computer/clipboard/read", + path_template("/browsers/{id}/computer/clipboard/read", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -1109,7 +1109,7 @@ async def scroll( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._post( - f"/browsers/{id}/computer/scroll", + path_template("/browsers/{id}/computer/scroll", id=id), body=await async_maybe_transform( { "x": x, @@ -1155,7 +1155,7 @@ async def set_cursor_visibility( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/browsers/{id}/computer/cursor", + path_template("/browsers/{id}/computer/cursor", id=id), body=await async_maybe_transform( {"hidden": hidden}, computer_set_cursor_visibility_params.ComputerSetCursorVisibilityParams ), @@ -1198,7 +1198,7 @@ async def type_text( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._post( - f"/browsers/{id}/computer/type", + path_template("/browsers/{id}/computer/type", id=id), body=await async_maybe_transform( { "text": text, @@ -1242,7 +1242,7 @@ async def write_clipboard( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._post( - f"/browsers/{id}/computer/clipboard/write", + path_template("/browsers/{id}/computer/clipboard/write", id=id), body=await async_maybe_transform( {"text": text}, computer_write_clipboard_params.ComputerWriteClipboardParams ), diff --git a/src/kernel/resources/browsers/fs/fs.py b/src/kernel/resources/browsers/fs/fs.py index 1bd16afb..f26119fb 100644 --- a/src/kernel/resources/browsers/fs/fs.py +++ b/src/kernel/resources/browsers/fs/fs.py @@ -30,7 +30,7 @@ omit, not_given, ) -from ...._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform +from ...._utils import extract_files, path_template, maybe_transform, deepcopy_minimal, async_maybe_transform from ...._compat import cached_property from ...._resource import SyncAPIResource, AsyncAPIResource from ...._response import ( @@ -128,7 +128,7 @@ def create_directory( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._put( - f"/browsers/{id}/fs/create_directory", + path_template("/browsers/{id}/fs/create_directory", id=id), body=maybe_transform( { "path": path, @@ -172,7 +172,7 @@ def delete_directory( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._put( - f"/browsers/{id}/fs/delete_directory", + path_template("/browsers/{id}/fs/delete_directory", id=id), body=maybe_transform({"path": path}, f_delete_directory_params.FDeleteDirectoryParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -210,7 +210,7 @@ def delete_file( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._put( - f"/browsers/{id}/fs/delete_file", + path_template("/browsers/{id}/fs/delete_file", id=id), body=maybe_transform({"path": path}, f_delete_file_params.FDeleteFileParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -248,7 +248,7 @@ def download_dir_zip( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "application/zip", **(extra_headers or {})} return self._get( - f"/browsers/{id}/fs/download_dir_zip", + path_template("/browsers/{id}/fs/download_dir_zip", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -288,7 +288,7 @@ def file_info( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/browsers/{id}/fs/file_info", + path_template("/browsers/{id}/fs/file_info", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -328,7 +328,7 @@ def list_files( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/browsers/{id}/fs/list_files", + path_template("/browsers/{id}/fs/list_files", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -372,7 +372,7 @@ def move( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._put( - f"/browsers/{id}/fs/move", + path_template("/browsers/{id}/fs/move", id=id), body=maybe_transform( { "dest_path": dest_path, @@ -416,7 +416,7 @@ def read_file( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} return self._get( - f"/browsers/{id}/fs/read_file", + path_template("/browsers/{id}/fs/read_file", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -466,7 +466,7 @@ def set_file_permissions( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._put( - f"/browsers/{id}/fs/set_file_permissions", + path_template("/browsers/{id}/fs/set_file_permissions", id=id), body=maybe_transform( { "mode": mode, @@ -516,7 +516,7 @@ def upload( # multipart/form-data; boundary=---abc-- extra_headers["Content-Type"] = "multipart/form-data" return self._post( - f"/browsers/{id}/fs/upload", + path_template("/browsers/{id}/fs/upload", id=id), body=maybe_transform(body, f_upload_params.FUploadParams), files=extracted_files, options=make_request_options( @@ -567,7 +567,7 @@ def upload_zip( # multipart/form-data; boundary=---abc-- extra_headers["Content-Type"] = "multipart/form-data" return self._post( - f"/browsers/{id}/fs/upload_zip", + path_template("/browsers/{id}/fs/upload_zip", id=id), body=maybe_transform(body, f_upload_zip_params.FUploadZipParams), files=files, options=make_request_options( @@ -611,7 +611,7 @@ def write_file( extra_headers = {"Accept": "*/*", **(extra_headers or {})} extra_headers["Content-Type"] = "application/octet-stream" return self._put( - f"/browsers/{id}/fs/write_file", + path_template("/browsers/{id}/fs/write_file", id=id), content=read_file_content(contents) if isinstance(contents, os.PathLike) else contents, options=make_request_options( extra_headers=extra_headers, @@ -690,7 +690,7 @@ async def create_directory( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._put( - f"/browsers/{id}/fs/create_directory", + path_template("/browsers/{id}/fs/create_directory", id=id), body=await async_maybe_transform( { "path": path, @@ -734,7 +734,7 @@ async def delete_directory( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._put( - f"/browsers/{id}/fs/delete_directory", + path_template("/browsers/{id}/fs/delete_directory", id=id), body=await async_maybe_transform({"path": path}, f_delete_directory_params.FDeleteDirectoryParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -772,7 +772,7 @@ async def delete_file( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._put( - f"/browsers/{id}/fs/delete_file", + path_template("/browsers/{id}/fs/delete_file", id=id), body=await async_maybe_transform({"path": path}, f_delete_file_params.FDeleteFileParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -810,7 +810,7 @@ async def download_dir_zip( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "application/zip", **(extra_headers or {})} return await self._get( - f"/browsers/{id}/fs/download_dir_zip", + path_template("/browsers/{id}/fs/download_dir_zip", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -850,7 +850,7 @@ async def file_info( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/browsers/{id}/fs/file_info", + path_template("/browsers/{id}/fs/file_info", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -890,7 +890,7 @@ async def list_files( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/browsers/{id}/fs/list_files", + path_template("/browsers/{id}/fs/list_files", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -934,7 +934,7 @@ async def move( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._put( - f"/browsers/{id}/fs/move", + path_template("/browsers/{id}/fs/move", id=id), body=await async_maybe_transform( { "dest_path": dest_path, @@ -978,7 +978,7 @@ async def read_file( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} return await self._get( - f"/browsers/{id}/fs/read_file", + path_template("/browsers/{id}/fs/read_file", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -1028,7 +1028,7 @@ async def set_file_permissions( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._put( - f"/browsers/{id}/fs/set_file_permissions", + path_template("/browsers/{id}/fs/set_file_permissions", id=id), body=await async_maybe_transform( { "mode": mode, @@ -1078,7 +1078,7 @@ async def upload( # multipart/form-data; boundary=---abc-- extra_headers["Content-Type"] = "multipart/form-data" return await self._post( - f"/browsers/{id}/fs/upload", + path_template("/browsers/{id}/fs/upload", id=id), body=await async_maybe_transform(body, f_upload_params.FUploadParams), files=extracted_files, options=make_request_options( @@ -1129,7 +1129,7 @@ async def upload_zip( # multipart/form-data; boundary=---abc-- extra_headers["Content-Type"] = "multipart/form-data" return await self._post( - f"/browsers/{id}/fs/upload_zip", + path_template("/browsers/{id}/fs/upload_zip", id=id), body=await async_maybe_transform(body, f_upload_zip_params.FUploadZipParams), files=files, options=make_request_options( @@ -1173,7 +1173,7 @@ async def write_file( extra_headers = {"Accept": "*/*", **(extra_headers or {})} extra_headers["Content-Type"] = "application/octet-stream" return await self._put( - f"/browsers/{id}/fs/write_file", + path_template("/browsers/{id}/fs/write_file", id=id), content=await async_read_file_content(contents) if isinstance(contents, os.PathLike) else contents, options=make_request_options( extra_headers=extra_headers, diff --git a/src/kernel/resources/browsers/fs/watch.py b/src/kernel/resources/browsers/fs/watch.py index ca438673..bc046053 100644 --- a/src/kernel/resources/browsers/fs/watch.py +++ b/src/kernel/resources/browsers/fs/watch.py @@ -5,7 +5,7 @@ import httpx from ...._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given -from ...._utils import maybe_transform, async_maybe_transform +from ...._utils import path_template, maybe_transform, async_maybe_transform from ...._compat import cached_property from ...._resource import SyncAPIResource, AsyncAPIResource from ...._response import ( @@ -75,7 +75,7 @@ def events( raise ValueError(f"Expected a non-empty value for `watch_id` but received {watch_id!r}") extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} return self._get( - f"/browsers/{id}/fs/watch/{watch_id}/events", + path_template("/browsers/{id}/fs/watch/{watch_id}/events", id=id, watch_id=watch_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -116,7 +116,7 @@ def start( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/browsers/{id}/fs/watch", + path_template("/browsers/{id}/fs/watch", id=id), body=maybe_transform( { "path": path, @@ -160,7 +160,7 @@ def stop( raise ValueError(f"Expected a non-empty value for `watch_id` but received {watch_id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( - f"/browsers/{id}/fs/watch/{watch_id}", + path_template("/browsers/{id}/fs/watch/{watch_id}", id=id, watch_id=watch_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -220,7 +220,7 @@ async def events( raise ValueError(f"Expected a non-empty value for `watch_id` but received {watch_id!r}") extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} return await self._get( - f"/browsers/{id}/fs/watch/{watch_id}/events", + path_template("/browsers/{id}/fs/watch/{watch_id}/events", id=id, watch_id=watch_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -261,7 +261,7 @@ async def start( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/browsers/{id}/fs/watch", + path_template("/browsers/{id}/fs/watch", id=id), body=await async_maybe_transform( { "path": path, @@ -305,7 +305,7 @@ async def stop( raise ValueError(f"Expected a non-empty value for `watch_id` but received {watch_id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( - f"/browsers/{id}/fs/watch/{watch_id}", + path_template("/browsers/{id}/fs/watch/{watch_id}", id=id, watch_id=watch_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/kernel/resources/browsers/logs.py b/src/kernel/resources/browsers/logs.py index 01328551..35ee66dc 100644 --- a/src/kernel/resources/browsers/logs.py +++ b/src/kernel/resources/browsers/logs.py @@ -7,7 +7,7 @@ import httpx from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -81,7 +81,7 @@ def stream( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} return self._get( - f"/browsers/{id}/logs/stream", + path_template("/browsers/{id}/logs/stream", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -160,7 +160,7 @@ async def stream( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} return await self._get( - f"/browsers/{id}/logs/stream", + path_template("/browsers/{id}/logs/stream", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, diff --git a/src/kernel/resources/browsers/playwright.py b/src/kernel/resources/browsers/playwright.py index 6979a9de..8d261ed5 100644 --- a/src/kernel/resources/browsers/playwright.py +++ b/src/kernel/resources/browsers/playwright.py @@ -5,7 +5,7 @@ import httpx from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -82,7 +82,7 @@ def execute( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/browsers/{id}/playwright/execute", + path_template("/browsers/{id}/playwright/execute", id=id), body=maybe_transform( { "code": code, @@ -158,7 +158,7 @@ async def execute( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/browsers/{id}/playwright/execute", + path_template("/browsers/{id}/playwright/execute", id=id), body=await async_maybe_transform( { "code": code, diff --git a/src/kernel/resources/browsers/process.py b/src/kernel/resources/browsers/process.py index 86752a5e..83827d38 100644 --- a/src/kernel/resources/browsers/process.py +++ b/src/kernel/resources/browsers/process.py @@ -8,7 +8,7 @@ import httpx from ..._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -106,7 +106,7 @@ def exec( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/browsers/{id}/process/exec", + path_template("/browsers/{id}/process/exec", id=id), body=maybe_transform( { "command": command, @@ -157,7 +157,7 @@ def kill( if not process_id: raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}") return self._post( - f"/browsers/{id}/process/{process_id}/kill", + path_template("/browsers/{id}/process/{process_id}/kill", id=id, process_id=process_id), body=maybe_transform({"signal": signal}, process_kill_params.ProcessKillParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -200,7 +200,7 @@ def resize( if not process_id: raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}") return self._post( - f"/browsers/{id}/process/{process_id}/resize", + path_template("/browsers/{id}/process/{process_id}/resize", id=id, process_id=process_id), body=maybe_transform( { "cols": cols, @@ -270,7 +270,7 @@ def spawn( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/browsers/{id}/process/spawn", + path_template("/browsers/{id}/process/spawn", id=id), body=maybe_transform( { "command": command, @@ -321,7 +321,7 @@ def status( if not process_id: raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}") return self._get( - f"/browsers/{id}/process/{process_id}/status", + path_template("/browsers/{id}/process/{process_id}/status", id=id, process_id=process_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -360,7 +360,7 @@ def stdin( if not process_id: raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}") return self._post( - f"/browsers/{id}/process/{process_id}/stdin", + path_template("/browsers/{id}/process/{process_id}/stdin", id=id, process_id=process_id), body=maybe_transform({"data_b64": data_b64}, process_stdin_params.ProcessStdinParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -398,7 +398,7 @@ def stdout_stream( raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}") extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} return self._get( - f"/browsers/{id}/process/{process_id}/stdout/stream", + path_template("/browsers/{id}/process/{process_id}/stdout/stream", id=id, process_id=process_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -477,7 +477,7 @@ async def exec( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/browsers/{id}/process/exec", + path_template("/browsers/{id}/process/exec", id=id), body=await async_maybe_transform( { "command": command, @@ -528,7 +528,7 @@ async def kill( if not process_id: raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}") return await self._post( - f"/browsers/{id}/process/{process_id}/kill", + path_template("/browsers/{id}/process/{process_id}/kill", id=id, process_id=process_id), body=await async_maybe_transform({"signal": signal}, process_kill_params.ProcessKillParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -571,7 +571,7 @@ async def resize( if not process_id: raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}") return await self._post( - f"/browsers/{id}/process/{process_id}/resize", + path_template("/browsers/{id}/process/{process_id}/resize", id=id, process_id=process_id), body=await async_maybe_transform( { "cols": cols, @@ -641,7 +641,7 @@ async def spawn( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/browsers/{id}/process/spawn", + path_template("/browsers/{id}/process/spawn", id=id), body=await async_maybe_transform( { "command": command, @@ -692,7 +692,7 @@ async def status( if not process_id: raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}") return await self._get( - f"/browsers/{id}/process/{process_id}/status", + path_template("/browsers/{id}/process/{process_id}/status", id=id, process_id=process_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -731,7 +731,7 @@ async def stdin( if not process_id: raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}") return await self._post( - f"/browsers/{id}/process/{process_id}/stdin", + path_template("/browsers/{id}/process/{process_id}/stdin", id=id, process_id=process_id), body=await async_maybe_transform({"data_b64": data_b64}, process_stdin_params.ProcessStdinParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -769,7 +769,7 @@ async def stdout_stream( raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}") extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} return await self._get( - f"/browsers/{id}/process/{process_id}/stdout/stream", + path_template("/browsers/{id}/process/{process_id}/stdout/stream", id=id, process_id=process_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/kernel/resources/browsers/replays.py b/src/kernel/resources/browsers/replays.py index 743a6668..2b20953a 100644 --- a/src/kernel/resources/browsers/replays.py +++ b/src/kernel/resources/browsers/replays.py @@ -5,7 +5,7 @@ import httpx from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -78,7 +78,7 @@ def list( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/browsers/{id}/replays", + path_template("/browsers/{id}/replays", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -115,7 +115,7 @@ def download( raise ValueError(f"Expected a non-empty value for `replay_id` but received {replay_id!r}") extra_headers = {"Accept": "video/mp4", **(extra_headers or {})} return self._get( - f"/browsers/{id}/replays/{replay_id}", + path_template("/browsers/{id}/replays/{replay_id}", id=id, replay_id=replay_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -154,7 +154,7 @@ def start( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/browsers/{id}/replays", + path_template("/browsers/{id}/replays", id=id), body=maybe_transform( { "framerate": framerate, @@ -198,7 +198,7 @@ def stop( raise ValueError(f"Expected a non-empty value for `replay_id` but received {replay_id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._post( - f"/browsers/{id}/replays/{replay_id}/stop", + path_template("/browsers/{id}/replays/{replay_id}/stop", id=id, replay_id=replay_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -254,7 +254,7 @@ async def list( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/browsers/{id}/replays", + path_template("/browsers/{id}/replays", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -291,7 +291,7 @@ async def download( raise ValueError(f"Expected a non-empty value for `replay_id` but received {replay_id!r}") extra_headers = {"Accept": "video/mp4", **(extra_headers or {})} return await self._get( - f"/browsers/{id}/replays/{replay_id}", + path_template("/browsers/{id}/replays/{replay_id}", id=id, replay_id=replay_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -330,7 +330,7 @@ async def start( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/browsers/{id}/replays", + path_template("/browsers/{id}/replays", id=id), body=await async_maybe_transform( { "framerate": framerate, @@ -374,7 +374,7 @@ async def stop( raise ValueError(f"Expected a non-empty value for `replay_id` but received {replay_id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._post( - f"/browsers/{id}/replays/{replay_id}/stop", + path_template("/browsers/{id}/replays/{replay_id}/stop", id=id, replay_id=replay_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/kernel/resources/credential_providers.py b/src/kernel/resources/credential_providers.py index c7ad4b00..2dede2c4 100644 --- a/src/kernel/resources/credential_providers.py +++ b/src/kernel/resources/credential_providers.py @@ -8,7 +8,7 @@ from ..types import credential_provider_create_params, credential_provider_update_params from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -126,7 +126,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/org/credential_providers/{id}", + path_template("/org/credential_providers/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -174,7 +174,7 @@ def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._patch( - f"/org/credential_providers/{id}", + path_template("/org/credential_providers/{id}", id=id), body=maybe_transform( { "token": token, @@ -237,7 +237,7 @@ def delete( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( - f"/org/credential_providers/{id}", + path_template("/org/credential_providers/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -271,7 +271,7 @@ def list_items( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/org/credential_providers/{id}/items", + path_template("/org/credential_providers/{id}/items", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -304,7 +304,7 @@ def test( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/org/credential_providers/{id}/test", + path_template("/org/credential_providers/{id}/test", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -412,7 +412,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/org/credential_providers/{id}", + path_template("/org/credential_providers/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -460,7 +460,7 @@ async def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._patch( - f"/org/credential_providers/{id}", + path_template("/org/credential_providers/{id}", id=id), body=await async_maybe_transform( { "token": token, @@ -523,7 +523,7 @@ async def delete( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( - f"/org/credential_providers/{id}", + path_template("/org/credential_providers/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -557,7 +557,7 @@ async def list_items( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/org/credential_providers/{id}/items", + path_template("/org/credential_providers/{id}/items", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -590,7 +590,7 @@ async def test( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/org/credential_providers/{id}/test", + path_template("/org/credential_providers/{id}/test", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/kernel/resources/credentials.py b/src/kernel/resources/credentials.py index 000c7675..093fcc5d 100644 --- a/src/kernel/resources/credentials.py +++ b/src/kernel/resources/credentials.py @@ -8,7 +8,7 @@ from ..types import credential_list_params, credential_create_params, credential_update_params from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -133,7 +133,7 @@ def retrieve( if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return self._get( - f"/credentials/{id_or_name}", + path_template("/credentials/{id_or_name}", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -183,7 +183,7 @@ def update( if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return self._patch( - f"/credentials/{id_or_name}", + path_template("/credentials/{id_or_name}", id_or_name=id_or_name), body=maybe_transform( { "name": name, @@ -279,7 +279,7 @@ def delete( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( - f"/credentials/{id_or_name}", + path_template("/credentials/{id_or_name}", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -314,7 +314,7 @@ def totp_code( if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return self._get( - f"/credentials/{id_or_name}/totp-code", + path_template("/credentials/{id_or_name}/totp-code", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -430,7 +430,7 @@ async def retrieve( if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return await self._get( - f"/credentials/{id_or_name}", + path_template("/credentials/{id_or_name}", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -480,7 +480,7 @@ async def update( if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return await self._patch( - f"/credentials/{id_or_name}", + path_template("/credentials/{id_or_name}", id_or_name=id_or_name), body=await async_maybe_transform( { "name": name, @@ -576,7 +576,7 @@ async def delete( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( - f"/credentials/{id_or_name}", + path_template("/credentials/{id_or_name}", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -611,7 +611,7 @@ async def totp_code( if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return await self._get( - f"/credentials/{id_or_name}/totp-code", + path_template("/credentials/{id_or_name}/totp-code", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/kernel/resources/deployments.py b/src/kernel/resources/deployments.py index b6a72d2d..6b2c7601 100644 --- a/src/kernel/resources/deployments.py +++ b/src/kernel/resources/deployments.py @@ -9,7 +9,7 @@ from ..types import deployment_list_params, deployment_create_params, deployment_follow_params from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, FileTypes, omit, not_given -from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform +from .._utils import extract_files, path_template, maybe_transform, deepcopy_minimal, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -147,7 +147,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/deployments/{id}", + path_template("/deployments/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -239,7 +239,7 @@ def delete( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( - f"/deployments/{id}", + path_template("/deployments/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -278,7 +278,7 @@ def follow( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} return self._get( - f"/deployments/{id}/events", + path_template("/deployments/{id}/events", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -412,7 +412,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/deployments/{id}", + path_template("/deployments/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -504,7 +504,7 @@ async def delete( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( - f"/deployments/{id}", + path_template("/deployments/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -543,7 +543,7 @@ async def follow( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} return await self._get( - f"/deployments/{id}/events", + path_template("/deployments/{id}/events", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, diff --git a/src/kernel/resources/extensions.py b/src/kernel/resources/extensions.py index ffdef29e..c429b3c5 100644 --- a/src/kernel/resources/extensions.py +++ b/src/kernel/resources/extensions.py @@ -9,7 +9,7 @@ from ..types import extension_upload_params, extension_download_from_chrome_store_params from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, FileTypes, omit, not_given -from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform +from .._utils import extract_files, path_template, maybe_transform, deepcopy_minimal, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -101,7 +101,7 @@ def delete( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( - f"/extensions/{id_or_name}", + path_template("/extensions/{id_or_name}", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -135,7 +135,7 @@ def download( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} return self._get( - f"/extensions/{id_or_name}", + path_template("/extensions/{id_or_name}", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -310,7 +310,7 @@ async def delete( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( - f"/extensions/{id_or_name}", + path_template("/extensions/{id_or_name}", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -344,7 +344,7 @@ async def download( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} return await self._get( - f"/extensions/{id_or_name}", + path_template("/extensions/{id_or_name}", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/kernel/resources/invocations.py b/src/kernel/resources/invocations.py index 25be409f..6d367e9e 100644 --- a/src/kernel/resources/invocations.py +++ b/src/kernel/resources/invocations.py @@ -9,7 +9,7 @@ from ..types import invocation_list_params, invocation_create_params, invocation_follow_params, invocation_update_params from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -140,7 +140,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/invocations/{id}", + path_template("/invocations/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -181,7 +181,7 @@ def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._patch( - f"/invocations/{id}", + path_template("/invocations/{id}", id=id), body=maybe_transform( { "status": status, @@ -296,7 +296,7 @@ def delete_browsers( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( - f"/invocations/{id}/browsers", + path_template("/invocations/{id}/browsers", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -335,7 +335,7 @@ def follow( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} return self._get( - f"/invocations/{id}/events", + path_template("/invocations/{id}/events", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -376,7 +376,7 @@ def list_browsers( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/invocations/{id}/browsers", + path_template("/invocations/{id}/browsers", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -493,7 +493,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/invocations/{id}", + path_template("/invocations/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -534,7 +534,7 @@ async def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._patch( - f"/invocations/{id}", + path_template("/invocations/{id}", id=id), body=await async_maybe_transform( { "status": status, @@ -649,7 +649,7 @@ async def delete_browsers( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( - f"/invocations/{id}/browsers", + path_template("/invocations/{id}/browsers", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -688,7 +688,7 @@ async def follow( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} return await self._get( - f"/invocations/{id}/events", + path_template("/invocations/{id}/events", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -729,7 +729,7 @@ async def list_browsers( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/invocations/{id}/browsers", + path_template("/invocations/{id}/browsers", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/kernel/resources/profiles.py b/src/kernel/resources/profiles.py index f75569f2..ec3d3fcc 100644 --- a/src/kernel/resources/profiles.py +++ b/src/kernel/resources/profiles.py @@ -6,7 +6,7 @@ from ..types import profile_list_params, profile_create_params from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -113,7 +113,7 @@ def retrieve( if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return self._get( - f"/profiles/{id_or_name}", + path_template("/profiles/{id_or_name}", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -198,7 +198,7 @@ def delete( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( - f"/profiles/{id_or_name}", + path_template("/profiles/{id_or_name}", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -234,7 +234,7 @@ def download( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} return self._get( - f"/profiles/{id_or_name}/download", + path_template("/profiles/{id_or_name}/download", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -325,7 +325,7 @@ async def retrieve( if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return await self._get( - f"/profiles/{id_or_name}", + path_template("/profiles/{id_or_name}", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -410,7 +410,7 @@ async def delete( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( - f"/profiles/{id_or_name}", + path_template("/profiles/{id_or_name}", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -446,7 +446,7 @@ async def download( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} return await self._get( - f"/profiles/{id_or_name}/download", + path_template("/profiles/{id_or_name}/download", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/kernel/resources/proxies.py b/src/kernel/resources/proxies.py index 0c2508c0..f259d6ed 100644 --- a/src/kernel/resources/proxies.py +++ b/src/kernel/resources/proxies.py @@ -8,7 +8,7 @@ from ..types import proxy_create_params from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, SequenceNotStr, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -130,7 +130,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/proxies/{id}", + path_template("/proxies/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -184,7 +184,7 @@ def delete( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( - f"/proxies/{id}", + path_template("/proxies/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -217,7 +217,7 @@ def check( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/proxies/{id}/check", + path_template("/proxies/{id}/check", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -329,7 +329,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/proxies/{id}", + path_template("/proxies/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -383,7 +383,7 @@ async def delete( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( - f"/proxies/{id}", + path_template("/proxies/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -416,7 +416,7 @@ async def check( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/proxies/{id}/check", + path_template("/proxies/{id}/check", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/tests/test_utils/test_path.py b/tests/test_utils/test_path.py new file mode 100644 index 00000000..80b86802 --- /dev/null +++ b/tests/test_utils/test_path.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +from typing import Any + +import pytest + +from kernel._utils._path import path_template + + +@pytest.mark.parametrize( + "template, kwargs, expected", + [ + ("/v1/{id}", dict(id="abc"), "/v1/abc"), + ("/v1/{a}/{b}", dict(a="x", b="y"), "/v1/x/y"), + ("/v1/{a}{b}/path/{c}?val={d}#{e}", dict(a="x", b="y", c="z", d="u", e="v"), "/v1/xy/path/z?val=u#v"), + ("/{w}/{w}", dict(w="echo"), "/echo/echo"), + ("/v1/static", {}, "/v1/static"), + ("", {}, ""), + ("/v1/?q={n}&count=10", dict(n=42), "/v1/?q=42&count=10"), + ("/v1/{v}", dict(v=None), "/v1/null"), + ("/v1/{v}", dict(v=True), "/v1/true"), + ("/v1/{v}", dict(v=False), "/v1/false"), + ("/v1/{v}", dict(v=".hidden"), "/v1/.hidden"), # dot prefix ok + ("/v1/{v}", dict(v="file.txt"), "/v1/file.txt"), # dot in middle ok + ("/v1/{v}", dict(v="..."), "/v1/..."), # triple dot ok + ("/v1/{a}{b}", dict(a=".", b="txt"), "/v1/.txt"), # dot var combining with adjacent to be ok + ("/items?q={v}#{f}", dict(v=".", f=".."), "/items?q=.#.."), # dots in query/fragment are fine + ( + "/v1/{a}?query={b}", + dict(a="../../other/endpoint", b="a&bad=true"), + "/v1/..%2F..%2Fother%2Fendpoint?query=a%26bad%3Dtrue", + ), + ("/v1/{val}", dict(val="a/b/c"), "/v1/a%2Fb%2Fc"), + ("/v1/{val}", dict(val="a/b/c?query=value"), "/v1/a%2Fb%2Fc%3Fquery=value"), + ("/v1/{val}", dict(val="a/b/c?query=value&bad=true"), "/v1/a%2Fb%2Fc%3Fquery=value&bad=true"), + ("/v1/{val}", dict(val="%20"), "/v1/%2520"), # escapes escape sequences in input + # Query: slash and ? are safe, # is not + ("/items?q={v}", dict(v="a/b"), "/items?q=a/b"), + ("/items?q={v}", dict(v="a?b"), "/items?q=a?b"), + ("/items?q={v}", dict(v="a#b"), "/items?q=a%23b"), + ("/items?q={v}", dict(v="a b"), "/items?q=a%20b"), + # Fragment: slash and ? are safe + ("/docs#{v}", dict(v="a/b"), "/docs#a/b"), + ("/docs#{v}", dict(v="a?b"), "/docs#a?b"), + # Path: slash, ? and # are all encoded + ("/v1/{v}", dict(v="a/b"), "/v1/a%2Fb"), + ("/v1/{v}", dict(v="a?b"), "/v1/a%3Fb"), + ("/v1/{v}", dict(v="a#b"), "/v1/a%23b"), + # same var encoded differently by component + ( + "/v1/{v}?q={v}#{v}", + dict(v="a/b?c#d"), + "/v1/a%2Fb%3Fc%23d?q=a/b?c%23d#a/b?c%23d", + ), + ("/v1/{val}", dict(val="x?admin=true"), "/v1/x%3Fadmin=true"), # query injection + ("/v1/{val}", dict(val="x#admin"), "/v1/x%23admin"), # fragment injection + ], +) +def test_interpolation(template: str, kwargs: dict[str, Any], expected: str) -> None: + assert path_template(template, **kwargs) == expected + + +def test_missing_kwarg_raises_key_error() -> None: + with pytest.raises(KeyError, match="org_id"): + path_template("/v1/{org_id}") + + +@pytest.mark.parametrize( + "template, kwargs", + [ + ("{a}/path", dict(a=".")), + ("{a}/path", dict(a="..")), + ("/v1/{a}", dict(a=".")), + ("/v1/{a}", dict(a="..")), + ("/v1/{a}/path", dict(a=".")), + ("/v1/{a}/path", dict(a="..")), + ("/v1/{a}{b}", dict(a=".", b=".")), # adjacent vars → ".." + ("/v1/{a}.", dict(a=".")), # var + static → ".." + ("/v1/{a}{b}", dict(a="", b=".")), # empty + dot → "." + ("/v1/%2e/{x}", dict(x="ok")), # encoded dot in static text + ("/v1/%2e./{x}", dict(x="ok")), # mixed encoded ".." in static + ("/v1/.%2E/{x}", dict(x="ok")), # mixed encoded ".." in static + ("/v1/{v}?q=1", dict(v="..")), + ("/v1/{v}#frag", dict(v="..")), + ], +) +def test_dot_segment_rejected(template: str, kwargs: dict[str, Any]) -> None: + with pytest.raises(ValueError, match="dot-segment"): + path_template(template, **kwargs) From ece76c2f7d2a20af7c00988d161c4e275623aaac Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:33:10 +0000 Subject: [PATCH 10/11] feat: Enhance managed authentication with CUA support and new features --- .stats.yml | 8 +- api.md | 2 + src/kernel/resources/auth/connections.py | 181 +++++++++++++++++- src/kernel/types/auth/__init__.py | 1 + .../types/auth/connection_follow_response.py | 23 +++ .../types/auth/connection_submit_params.py | 16 +- .../types/auth/connection_update_params.py | 72 +++++++ src/kernel/types/auth/managed_auth.py | 24 ++- tests/api_resources/auth/test_connections.py | 132 +++++++++++++ 9 files changed, 448 insertions(+), 11 deletions(-) create mode 100644 src/kernel/types/auth/connection_update_params.py diff --git a/.stats.yml b/.stats.yml index ad84265a..be60802f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 103 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-17e50cf93d8052ff655c160fc0f156621d9029b041526d4e2e3317b13f80822f.yml -openapi_spec_hash: f7dadc8d93e77983936eb18a8080ce15 -config_hash: cff4d43372b6fa66b64e2d4150f6aa76 +configured_endpoints: 104 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-bb2ac8e0d3a1c08e8afcbcbad7cb733d0f84bd22a8d233c1ec3100a01ee078ae.yml +openapi_spec_hash: a83f7d1c422c85d6dc6158af7afe1d09 +config_hash: 16e4457a0bb26e98a335a1c2a572290a diff --git a/api.md b/api.md index cce73021..696c481b 100644 --- a/api.md +++ b/api.md @@ -245,6 +245,7 @@ from kernel.types.auth import ( LoginResponse, ManagedAuth, ManagedAuthCreateRequest, + ManagedAuthUpdateRequest, SubmitFieldsRequest, SubmitFieldsResponse, ConnectionFollowResponse, @@ -255,6 +256,7 @@ Methods: - client.auth.connections.create(\*\*params) -> ManagedAuth - client.auth.connections.retrieve(id) -> ManagedAuth +- client.auth.connections.update(id, \*\*params) -> ManagedAuth - client.auth.connections.list(\*\*params) -> SyncOffsetPagination[ManagedAuth] - client.auth.connections.delete(id) -> None - client.auth.connections.follow(id) -> ConnectionFollowResponse diff --git a/src/kernel/resources/auth/connections.py b/src/kernel/resources/auth/connections.py index 0915365b..c610da4c 100644 --- a/src/kernel/resources/auth/connections.py +++ b/src/kernel/resources/auth/connections.py @@ -23,6 +23,7 @@ connection_login_params, connection_create_params, connection_submit_params, + connection_update_params, ) from ..._base_client import AsyncPaginator, make_request_options from ...types.auth.managed_auth import ManagedAuth @@ -186,6 +187,76 @@ def retrieve( cast_to=ManagedAuth, ) + def update( + self, + id: str, + *, + allowed_domains: SequenceNotStr[str] | Omit = omit, + credential: connection_update_params.Credential | Omit = omit, + health_check_interval: int | Omit = omit, + login_url: str | Omit = omit, + proxy: connection_update_params.Proxy | Omit = omit, + save_credentials: bool | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ManagedAuth: + """Update an auth connection's configuration. + + Only the fields provided will be + updated. + + Args: + allowed_domains: Additional domains valid for this auth flow (replaces existing list) + + credential: + Reference to credentials for the auth connection. Use one of: + + - { name } for Kernel credentials + - { provider, path } for external provider item + - { provider, auto: true } for external provider domain lookup + + health_check_interval: Interval in seconds between automatic health checks + + login_url: Login page URL. Set to empty string to clear. + + proxy: Proxy selection. Provide either id or name. The proxy must belong to the + caller's org. + + save_credentials: Whether to save credentials after every successful login + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._patch( + path_template("/auth/connections/{id}", id=id), + body=maybe_transform( + { + "allowed_domains": allowed_domains, + "credential": credential, + "health_check_interval": health_check_interval, + "login_url": login_url, + "proxy": proxy, + "save_credentials": save_credentials, + }, + connection_update_params.ConnectionUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ManagedAuth, + ) + def list( self, *, @@ -367,7 +438,9 @@ def submit( *, fields: Dict[str, str] | Omit = omit, mfa_option_id: str | Omit = omit, + sign_in_option_id: str | Omit = omit, sso_button_selector: str | Omit = omit, + sso_provider: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -383,9 +456,15 @@ def submit( Args: fields: Map of field name to value - mfa_option_id: Optional MFA option ID if user selected an MFA method + mfa_option_id: The MFA method type to select (when mfa_options were returned) + + sign_in_option_id: The sign-in option ID to select (when sign_in_options were returned) - sso_button_selector: Optional XPath selector if user chose to click an SSO button instead + sso_button_selector: XPath selector for the SSO button to click (ODA). Use sso_provider instead for + CUA. + + sso_provider: SSO provider to click, matching the provider field from pending_sso_buttons + (e.g., "google", "github"). Cannot be used with sso_button_selector. extra_headers: Send extra headers @@ -403,7 +482,9 @@ def submit( { "fields": fields, "mfa_option_id": mfa_option_id, + "sign_in_option_id": sign_in_option_id, "sso_button_selector": sso_button_selector, + "sso_provider": sso_provider, }, connection_submit_params.ConnectionSubmitParams, ), @@ -567,6 +648,76 @@ async def retrieve( cast_to=ManagedAuth, ) + async def update( + self, + id: str, + *, + allowed_domains: SequenceNotStr[str] | Omit = omit, + credential: connection_update_params.Credential | Omit = omit, + health_check_interval: int | Omit = omit, + login_url: str | Omit = omit, + proxy: connection_update_params.Proxy | Omit = omit, + save_credentials: bool | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ManagedAuth: + """Update an auth connection's configuration. + + Only the fields provided will be + updated. + + Args: + allowed_domains: Additional domains valid for this auth flow (replaces existing list) + + credential: + Reference to credentials for the auth connection. Use one of: + + - { name } for Kernel credentials + - { provider, path } for external provider item + - { provider, auto: true } for external provider domain lookup + + health_check_interval: Interval in seconds between automatic health checks + + login_url: Login page URL. Set to empty string to clear. + + proxy: Proxy selection. Provide either id or name. The proxy must belong to the + caller's org. + + save_credentials: Whether to save credentials after every successful login + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._patch( + path_template("/auth/connections/{id}", id=id), + body=await async_maybe_transform( + { + "allowed_domains": allowed_domains, + "credential": credential, + "health_check_interval": health_check_interval, + "login_url": login_url, + "proxy": proxy, + "save_credentials": save_credentials, + }, + connection_update_params.ConnectionUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ManagedAuth, + ) + def list( self, *, @@ -748,7 +899,9 @@ async def submit( *, fields: Dict[str, str] | Omit = omit, mfa_option_id: str | Omit = omit, + sign_in_option_id: str | Omit = omit, sso_button_selector: str | Omit = omit, + sso_provider: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -764,9 +917,15 @@ async def submit( Args: fields: Map of field name to value - mfa_option_id: Optional MFA option ID if user selected an MFA method + mfa_option_id: The MFA method type to select (when mfa_options were returned) + + sign_in_option_id: The sign-in option ID to select (when sign_in_options were returned) - sso_button_selector: Optional XPath selector if user chose to click an SSO button instead + sso_button_selector: XPath selector for the SSO button to click (ODA). Use sso_provider instead for + CUA. + + sso_provider: SSO provider to click, matching the provider field from pending_sso_buttons + (e.g., "google", "github"). Cannot be used with sso_button_selector. extra_headers: Send extra headers @@ -784,7 +943,9 @@ async def submit( { "fields": fields, "mfa_option_id": mfa_option_id, + "sign_in_option_id": sign_in_option_id, "sso_button_selector": sso_button_selector, + "sso_provider": sso_provider, }, connection_submit_params.ConnectionSubmitParams, ), @@ -805,6 +966,9 @@ def __init__(self, connections: ConnectionsResource) -> None: self.retrieve = to_raw_response_wrapper( connections.retrieve, ) + self.update = to_raw_response_wrapper( + connections.update, + ) self.list = to_raw_response_wrapper( connections.list, ) @@ -832,6 +996,9 @@ def __init__(self, connections: AsyncConnectionsResource) -> None: self.retrieve = async_to_raw_response_wrapper( connections.retrieve, ) + self.update = async_to_raw_response_wrapper( + connections.update, + ) self.list = async_to_raw_response_wrapper( connections.list, ) @@ -859,6 +1026,9 @@ def __init__(self, connections: ConnectionsResource) -> None: self.retrieve = to_streamed_response_wrapper( connections.retrieve, ) + self.update = to_streamed_response_wrapper( + connections.update, + ) self.list = to_streamed_response_wrapper( connections.list, ) @@ -886,6 +1056,9 @@ def __init__(self, connections: AsyncConnectionsResource) -> None: self.retrieve = async_to_streamed_response_wrapper( connections.retrieve, ) + self.update = async_to_streamed_response_wrapper( + connections.update, + ) self.list = async_to_streamed_response_wrapper( connections.list, ) diff --git a/src/kernel/types/auth/__init__.py b/src/kernel/types/auth/__init__.py index 51e505bf..db897944 100644 --- a/src/kernel/types/auth/__init__.py +++ b/src/kernel/types/auth/__init__.py @@ -9,4 +9,5 @@ from .connection_login_params import ConnectionLoginParams as ConnectionLoginParams from .connection_create_params import ConnectionCreateParams as ConnectionCreateParams from .connection_submit_params import ConnectionSubmitParams as ConnectionSubmitParams +from .connection_update_params import ConnectionUpdateParams as ConnectionUpdateParams from .connection_follow_response import ConnectionFollowResponse as ConnectionFollowResponse diff --git a/src/kernel/types/auth/connection_follow_response.py b/src/kernel/types/auth/connection_follow_response.py index 06ffaeab..4eeba5c5 100644 --- a/src/kernel/types/auth/connection_follow_response.py +++ b/src/kernel/types/auth/connection_follow_response.py @@ -15,6 +15,7 @@ "ManagedAuthStateEventDiscoveredField", "ManagedAuthStateEventMfaOption", "ManagedAuthStateEventPendingSSOButton", + "ManagedAuthStateEventSignInOption", ] @@ -77,6 +78,22 @@ class ManagedAuthStateEventPendingSSOButton(BaseModel): """XPath selector for the button""" +class ManagedAuthStateEventSignInOption(BaseModel): + """A non-MFA choice presented during the auth flow (e.g. + + account selection, org picker) + """ + + id: str + """Unique identifier for this option (used to submit selection back)""" + + label: str + """Display text for the option""" + + description: Optional[str] = None + """Additional context such as email address or org name""" + + class ManagedAuthStateEvent(BaseModel): """An event representing the current state of a managed auth flow.""" @@ -128,6 +145,12 @@ class ManagedAuthStateEvent(BaseModel): post_login_url: Optional[str] = None """URL where the browser landed after successful login.""" + sign_in_options: Optional[List[ManagedAuthStateEventSignInOption]] = None + """ + Non-MFA choices presented during the auth flow, such as account selection or org + pickers (present when flow_step=AWAITING_INPUT). + """ + website_error: Optional[str] = None """Visible error message from the website (e.g., 'Incorrect password'). diff --git a/src/kernel/types/auth/connection_submit_params.py b/src/kernel/types/auth/connection_submit_params.py index 0e2306ac..f785b856 100644 --- a/src/kernel/types/auth/connection_submit_params.py +++ b/src/kernel/types/auth/connection_submit_params.py @@ -13,7 +13,19 @@ class ConnectionSubmitParams(TypedDict, total=False): """Map of field name to value""" mfa_option_id: str - """Optional MFA option ID if user selected an MFA method""" + """The MFA method type to select (when mfa_options were returned)""" + + sign_in_option_id: str + """The sign-in option ID to select (when sign_in_options were returned)""" sso_button_selector: str - """Optional XPath selector if user chose to click an SSO button instead""" + """XPath selector for the SSO button to click (ODA). + + Use sso_provider instead for CUA. + """ + + sso_provider: str + """ + SSO provider to click, matching the provider field from pending_sso_buttons + (e.g., "google", "github"). Cannot be used with sso_button_selector. + """ diff --git a/src/kernel/types/auth/connection_update_params.py b/src/kernel/types/auth/connection_update_params.py new file mode 100644 index 00000000..77e738b7 --- /dev/null +++ b/src/kernel/types/auth/connection_update_params.py @@ -0,0 +1,72 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +from ..._types import SequenceNotStr + +__all__ = ["ConnectionUpdateParams", "Credential", "Proxy"] + + +class ConnectionUpdateParams(TypedDict, total=False): + allowed_domains: SequenceNotStr[str] + """Additional domains valid for this auth flow (replaces existing list)""" + + credential: Credential + """Reference to credentials for the auth connection. Use one of: + + - { name } for Kernel credentials + - { provider, path } for external provider item + - { provider, auto: true } for external provider domain lookup + """ + + health_check_interval: int + """Interval in seconds between automatic health checks""" + + login_url: str + """Login page URL. Set to empty string to clear.""" + + proxy: Proxy + """Proxy selection. + + Provide either id or name. The proxy must belong to the caller's org. + """ + + save_credentials: bool + """Whether to save credentials after every successful login""" + + +class Credential(TypedDict, total=False): + """Reference to credentials for the auth connection. + + Use one of: + - { name } for Kernel credentials + - { provider, path } for external provider item + - { provider, auto: true } for external provider domain lookup + """ + + auto: bool + """If true, lookup by domain from the specified provider""" + + name: str + """Kernel credential name""" + + path: str + """Provider-specific path (e.g., "VaultName/ItemName" for 1Password)""" + + provider: str + """External provider name (e.g., "my-1p")""" + + +class Proxy(TypedDict, total=False): + """Proxy selection. + + Provide either id or name. The proxy must belong to the caller's org. + """ + + id: str + """Proxy ID""" + + name: str + """Proxy name""" diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py index de607c9e..9f90bc5d 100644 --- a/src/kernel/types/auth/managed_auth.py +++ b/src/kernel/types/auth/managed_auth.py @@ -6,7 +6,7 @@ from ..._models import BaseModel -__all__ = ["ManagedAuth", "Credential", "DiscoveredField", "MfaOption", "PendingSSOButton"] +__all__ = ["ManagedAuth", "Credential", "DiscoveredField", "MfaOption", "PendingSSOButton", "SignInOption"] class Credential(BaseModel): @@ -90,6 +90,22 @@ class PendingSSOButton(BaseModel): """XPath selector for the button""" +class SignInOption(BaseModel): + """A non-MFA choice presented during the auth flow (e.g. + + account selection, org picker) + """ + + id: str + """Unique identifier for this option (used to submit selection back)""" + + label: str + """Display text for the option""" + + description: Optional[str] = None + """Additional context such as email address or org name""" + + class ManagedAuth(BaseModel): """Managed authentication that keeps a profile logged into a specific domain. @@ -214,6 +230,12 @@ class ManagedAuth(BaseModel): proxy_id: Optional[str] = None """ID of the proxy associated with this connection, if any.""" + sign_in_options: Optional[List[SignInOption]] = None + """ + Non-MFA choices presented during the auth flow, such as account selection or org + pickers (present when flow_step=awaiting_input). + """ + sso_provider: Optional[str] = None """SSO provider being used (e.g., google, github, microsoft)""" diff --git a/tests/api_resources/auth/test_connections.py b/tests/api_resources/auth/test_connections.py index e71a1736..f5edd89d 100644 --- a/tests/api_resources/auth/test_connections.py +++ b/tests/api_resources/auth/test_connections.py @@ -124,6 +124,70 @@ def test_path_params_retrieve(self, client: Kernel) -> None: "", ) + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update(self, client: Kernel) -> None: + connection = client.auth.connections.update( + id="id", + ) + assert_matches_type(ManagedAuth, connection, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update_with_all_params(self, client: Kernel) -> None: + connection = client.auth.connections.update( + id="id", + allowed_domains=["login.netflix.com", "auth.netflix.com"], + credential={ + "auto": True, + "name": "my-netflix-creds", + "path": "Personal/Netflix", + "provider": "my-1p", + }, + health_check_interval=3600, + login_url="https://netflix.com/login", + proxy={ + "id": "id", + "name": "name", + }, + save_credentials=True, + ) + assert_matches_type(ManagedAuth, connection, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_update(self, client: Kernel) -> None: + response = client.auth.connections.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + connection = response.parse() + assert_matches_type(ManagedAuth, connection, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_update(self, client: Kernel) -> None: + with client.auth.connections.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + connection = response.parse() + assert_matches_type(ManagedAuth, connection, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_update(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.auth.connections.with_raw_response.update( + id="", + ) + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_list(self, client: Kernel) -> None: @@ -318,7 +382,9 @@ def test_method_submit_with_all_params(self, client: Kernel) -> None: "password": "secret", }, mfa_option_id="sms", + sign_in_option_id="work-account", sso_button_selector="xpath=//button[contains(text(), 'Continue with Google')]", + sso_provider="google", ) assert_matches_type(SubmitFieldsResponse, connection, path=["response"]) @@ -464,6 +530,70 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: "", ) + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update(self, async_client: AsyncKernel) -> None: + connection = await async_client.auth.connections.update( + id="id", + ) + assert_matches_type(ManagedAuth, connection, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> None: + connection = await async_client.auth.connections.update( + id="id", + allowed_domains=["login.netflix.com", "auth.netflix.com"], + credential={ + "auto": True, + "name": "my-netflix-creds", + "path": "Personal/Netflix", + "provider": "my-1p", + }, + health_check_interval=3600, + login_url="https://netflix.com/login", + proxy={ + "id": "id", + "name": "name", + }, + save_credentials=True, + ) + assert_matches_type(ManagedAuth, connection, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_update(self, async_client: AsyncKernel) -> None: + response = await async_client.auth.connections.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + connection = await response.parse() + assert_matches_type(ManagedAuth, connection, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_update(self, async_client: AsyncKernel) -> None: + async with async_client.auth.connections.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + connection = await response.parse() + assert_matches_type(ManagedAuth, connection, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_update(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.auth.connections.with_raw_response.update( + id="", + ) + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_list(self, async_client: AsyncKernel) -> None: @@ -658,7 +788,9 @@ async def test_method_submit_with_all_params(self, async_client: AsyncKernel) -> "password": "secret", }, mfa_option_id="sms", + sign_in_option_id="work-account", sso_button_selector="xpath=//button[contains(text(), 'Continue with Google')]", + sso_provider="google", ) assert_matches_type(SubmitFieldsResponse, connection, path=["response"]) From 42276bf5e15fb01c2ba8ec4268da183182c52898 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:33:33 +0000 Subject: [PATCH 11/11] release: 0.44.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 25 +++++++++++++++++++++++++ pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index fe87cd91..cc51f6f8 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.43.0" + ".": "0.44.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cf89701..a11503f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## 0.44.0 (2026-03-20) + +Full Changelog: [v0.43.0...v0.44.0](https://github.com/kernel/kernel-python-sdk/compare/v0.43.0...v0.44.0) + +### Features + +* Add GPU viewport presets and GPU encoder defaults ([0735b45](https://github.com/kernel/kernel-python-sdk/commit/0735b45fc92950cef3afea92a879712ea0ebdf0f)) +* Adds description to OAS spec for docs about delta_x, delta_y ([9841aac](https://github.com/kernel/kernel-python-sdk/commit/9841aac21588beb0c6a22baf5c5b0bf8e8cdd024)) +* Drop headless GPU support and disable pooling ([cda8f94](https://github.com/kernel/kernel-python-sdk/commit/cda8f94f6cebabc3b3b6f95aff765816255a9270)) +* Enhance managed authentication with CUA support and new features ([ece76c2](https://github.com/kernel/kernel-python-sdk/commit/ece76c2f7d2a20af7c00988d161c4e275623aaac)) +* expose smooth drag mouse movement via public API ([c6f6862](https://github.com/kernel/kernel-python-sdk/commit/c6f6862d03620bb218a99c85431e384c5c8e5e4c)) +* Rename hardware acceleration UI/docs wording to GPU acceleration ([9ee4d0c](https://github.com/kernel/kernel-python-sdk/commit/9ee4d0c080da7f649a3bde63640593f33d5d0f6b)) + + +### Bug Fixes + +* **deps:** bump minimum typing-extensions version ([fd55947](https://github.com/kernel/kernel-python-sdk/commit/fd55947f1776b43cca804622d0c771ebe99ead60)) +* **pydantic:** do not pass `by_alias` unless set ([a815a82](https://github.com/kernel/kernel-python-sdk/commit/a815a8237cce2098b2f5d6a8ad5def400d418fbb)) +* sanitize endpoint path params ([9b55d2b](https://github.com/kernel/kernel-python-sdk/commit/9b55d2be9472f779fe20d7d863dacae1030d2b49)) + + +### Chores + +* **internal:** tweak CI branches ([8781c7d](https://github.com/kernel/kernel-python-sdk/commit/8781c7d9aa8759e487e5d86cbb82bfb7eb3d314e)) + ## 0.43.0 (2026-03-10) Full Changelog: [v0.42.1...v0.43.0](https://github.com/kernel/kernel-python-sdk/compare/v0.42.1...v0.43.0) diff --git a/pyproject.toml b/pyproject.toml index 62a14748..62ca8334 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.43.0" +version = "0.44.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 068f52c7..2c4ce41e 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.43.0" # x-release-please-version +__version__ = "0.44.0" # x-release-please-version