Skip to content

Commit 1d70eff

Browse files
feat: Add mid-session browser profile saves
Stainless-Generated-From: 4fbf9561f04097a8d4949d901b8ca0db4f778769
1 parent f5ee43e commit 1d70eff

6 files changed

Lines changed: 298 additions & 1 deletion

File tree

‎.stats.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
configured_endpoints: 175
1+
configured_endpoints: 176

‎api.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ from kernel.types import (
138138
BrowserUsage,
139139
BrowserVideoMemory,
140140
Profile,
141+
SaveProfileRequest,
141142
Tags,
142143
VaultReference,
143144
BrowserCreateResponse,
@@ -158,6 +159,7 @@ Methods:
158159
- <code title="delete /browsers/{id_or_name}">client.browsers.<a href="./src/kernel/resources/browsers/browsers.py">delete_by_id</a>(id_or_name) -> None</code>
159160
- <code title="post /browsers/{id_or_name}/extensions">client.browsers.<a href="./src/kernel/resources/browsers/browsers.py">load_extensions</a>(id_or_name, \*\*<a href="src/kernel/types/browser_load_extensions_params.py">params</a>) -> None</code>
160161
- <code title="post /browsers/{id_or_name}/repl">client.browsers.<a href="./src/kernel/resources/browsers/browsers.py">repl</a>(id_or_name, \*\*<a href="src/kernel/types/browser_repl_params.py">params</a>) -> <a href="./src/kernel/types/browser_repl_result.py">BrowserReplResult</a></code>
162+
- <code title="post /browsers/{id_or_name}/profiles">client.browsers.<a href="./src/kernel/resources/browsers/browsers.py">save_profile</a>(id_or_name, \*\*<a href="src/kernel/types/browser_save_profile_params.py">params</a>) -> <a href="./src/kernel/types/profile.py">Profile</a></code>
161163

162164
## Telemetry
163165

‎src/kernel/resources/browsers/browsers.py‎

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
browser_create_params,
3434
browser_update_params,
3535
browser_retrieve_params,
36+
browser_save_profile_params,
3637
browser_load_extensions_params,
3738
)
3839
from .process import (
@@ -88,6 +89,7 @@
8889
AsyncWebmcpResourceWithStreamingResponse,
8990
)
9091
from ..._base_client import AsyncPaginator, make_request_options
92+
from ...types.profile import Profile
9193
from ...types.tags_param import TagsParam
9294
from .playwright.playwright import (
9395
PlaywrightResource,
@@ -897,6 +899,86 @@ def repl(
897899
cast_to=BrowserReplResult,
898900
)
899901

902+
def save_profile(
903+
self,
904+
id_or_name: str,
905+
*,
906+
id: str | Omit = omit,
907+
name: str | Omit = omit,
908+
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
909+
# The extra values given here take precedence over values defined on the client or passed to this method.
910+
extra_headers: Headers | None = None,
911+
extra_query: Query | None = None,
912+
extra_body: Body | None = None,
913+
timeout: float | httpx.Timeout | None | NotGiven = not_given,
914+
idempotency_key: str | None = None,
915+
) -> Profile:
916+
"""
917+
Saves the running browser's current user-data state to an existing or new
918+
profile without ending the browser session.
919+
920+
Saving restarts the browser. Active CDP and WebDriver connections are closed and
921+
must reconnect, and WebDriver clients must create a new session. Open tabs are
922+
restored, but pages reload and in-memory page state is lost. The browser is
923+
stopped only while its user data is copied; this request returns once the
924+
restarted browser accepts CDP connections again and the profile has been
925+
uploaded. Caches, Service Worker storage and Google Drive IndexedDB data are
926+
left out of the saved profile but kept in the running browser.
927+
928+
This does not attach the destination profile to the browser or change its
929+
save_changes setting. Active browsers that already loaded the destination
930+
profile keep their in-memory state. Pools with refresh_on_profile_update enabled
931+
flush idle sessions and replenish them from the new snapshot; other browsers
932+
receive it the next time they load the profile. When saves made with this
933+
endpoint to the same profile overlap, the save that started most recently wins,
934+
even if an earlier one finishes later. The automatic save at the end of a
935+
session whose profile has save_changes enabled is ordered against these saves by
936+
when each finishes.
937+
938+
A successful save restarts the browser's inactivity timeout (timeout_seconds). A
939+
failed save leaves it unchanged.
940+
941+
Returns 409 when the browser is no longer active, when a pooled browser was
942+
released or reacquired after the request was made (browser_lease_changed), or
943+
when the browser is briefly unable to accept a save (browser_busy), in which
944+
case the request can be retried.
945+
946+
Args:
947+
id: Existing profile ID to overwrite.
948+
949+
name: Profile name to overwrite or create.
950+
951+
extra_headers: Send extra headers
952+
953+
extra_query: Add additional query parameters to the request
954+
955+
extra_body: Add additional JSON properties to the request
956+
957+
timeout: Override the client-level default timeout for this request, in seconds
958+
959+
idempotency_key: Specify a custom idempotency key for this request
960+
"""
961+
if not id_or_name:
962+
raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
963+
return self._post(
964+
path_template("/browsers/{id_or_name}/profiles", id_or_name=id_or_name),
965+
body=maybe_transform(
966+
{
967+
"id": id,
968+
"name": name,
969+
},
970+
browser_save_profile_params.BrowserSaveProfileParams,
971+
),
972+
options=make_request_options(
973+
extra_headers=extra_headers,
974+
extra_query=extra_query,
975+
extra_body=extra_body,
976+
timeout=timeout,
977+
idempotency_key=idempotency_key,
978+
),
979+
cast_to=Profile,
980+
)
981+
900982

901983
class AsyncBrowsersResource(AsyncAPIResource):
902984
@cached_property
@@ -1671,6 +1753,86 @@ async def repl(
16711753
cast_to=BrowserReplResult,
16721754
)
16731755

1756+
async def save_profile(
1757+
self,
1758+
id_or_name: str,
1759+
*,
1760+
id: str | Omit = omit,
1761+
name: str | Omit = omit,
1762+
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
1763+
# The extra values given here take precedence over values defined on the client or passed to this method.
1764+
extra_headers: Headers | None = None,
1765+
extra_query: Query | None = None,
1766+
extra_body: Body | None = None,
1767+
timeout: float | httpx.Timeout | None | NotGiven = not_given,
1768+
idempotency_key: str | None = None,
1769+
) -> Profile:
1770+
"""
1771+
Saves the running browser's current user-data state to an existing or new
1772+
profile without ending the browser session.
1773+
1774+
Saving restarts the browser. Active CDP and WebDriver connections are closed and
1775+
must reconnect, and WebDriver clients must create a new session. Open tabs are
1776+
restored, but pages reload and in-memory page state is lost. The browser is
1777+
stopped only while its user data is copied; this request returns once the
1778+
restarted browser accepts CDP connections again and the profile has been
1779+
uploaded. Caches, Service Worker storage and Google Drive IndexedDB data are
1780+
left out of the saved profile but kept in the running browser.
1781+
1782+
This does not attach the destination profile to the browser or change its
1783+
save_changes setting. Active browsers that already loaded the destination
1784+
profile keep their in-memory state. Pools with refresh_on_profile_update enabled
1785+
flush idle sessions and replenish them from the new snapshot; other browsers
1786+
receive it the next time they load the profile. When saves made with this
1787+
endpoint to the same profile overlap, the save that started most recently wins,
1788+
even if an earlier one finishes later. The automatic save at the end of a
1789+
session whose profile has save_changes enabled is ordered against these saves by
1790+
when each finishes.
1791+
1792+
A successful save restarts the browser's inactivity timeout (timeout_seconds). A
1793+
failed save leaves it unchanged.
1794+
1795+
Returns 409 when the browser is no longer active, when a pooled browser was
1796+
released or reacquired after the request was made (browser_lease_changed), or
1797+
when the browser is briefly unable to accept a save (browser_busy), in which
1798+
case the request can be retried.
1799+
1800+
Args:
1801+
id: Existing profile ID to overwrite.
1802+
1803+
name: Profile name to overwrite or create.
1804+
1805+
extra_headers: Send extra headers
1806+
1807+
extra_query: Add additional query parameters to the request
1808+
1809+
extra_body: Add additional JSON properties to the request
1810+
1811+
timeout: Override the client-level default timeout for this request, in seconds
1812+
1813+
idempotency_key: Specify a custom idempotency key for this request
1814+
"""
1815+
if not id_or_name:
1816+
raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
1817+
return await self._post(
1818+
path_template("/browsers/{id_or_name}/profiles", id_or_name=id_or_name),
1819+
body=await async_maybe_transform(
1820+
{
1821+
"id": id,
1822+
"name": name,
1823+
},
1824+
browser_save_profile_params.BrowserSaveProfileParams,
1825+
),
1826+
options=make_request_options(
1827+
extra_headers=extra_headers,
1828+
extra_query=extra_query,
1829+
extra_body=extra_body,
1830+
timeout=timeout,
1831+
idempotency_key=idempotency_key,
1832+
),
1833+
cast_to=Profile,
1834+
)
1835+
16741836

16751837
class BrowsersResourceWithRawResponse:
16761838
def __init__(self, browsers: BrowsersResource) -> None:
@@ -1700,6 +1862,9 @@ def __init__(self, browsers: BrowsersResource) -> None:
17001862
self.repl = to_raw_response_wrapper(
17011863
browsers.repl,
17021864
)
1865+
self.save_profile = to_raw_response_wrapper(
1866+
browsers.save_profile,
1867+
)
17031868

17041869
@cached_property
17051870
def telemetry(self) -> TelemetryResourceWithRawResponse:
@@ -1774,6 +1939,9 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None:
17741939
self.repl = async_to_raw_response_wrapper(
17751940
browsers.repl,
17761941
)
1942+
self.save_profile = async_to_raw_response_wrapper(
1943+
browsers.save_profile,
1944+
)
17771945

17781946
@cached_property
17791947
def telemetry(self) -> AsyncTelemetryResourceWithRawResponse:
@@ -1848,6 +2016,9 @@ def __init__(self, browsers: BrowsersResource) -> None:
18482016
self.repl = to_streamed_response_wrapper(
18492017
browsers.repl,
18502018
)
2019+
self.save_profile = to_streamed_response_wrapper(
2020+
browsers.save_profile,
2021+
)
18512022

18522023
@cached_property
18532024
def telemetry(self) -> TelemetryResourceWithStreamingResponse:
@@ -1922,6 +2093,9 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None:
19222093
self.repl = async_to_streamed_response_wrapper(
19232094
browsers.repl,
19242095
)
2096+
self.save_profile = async_to_streamed_response_wrapper(
2097+
browsers.save_profile,
2098+
)
19252099

19262100
@cached_property
19272101
def telemetry(self) -> AsyncTelemetryResourceWithStreamingResponse:

‎src/kernel/types/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@
135135
from .invocation_update_response import InvocationUpdateResponse as InvocationUpdateResponse
136136
from .browser_pool_acquire_params import BrowserPoolAcquireParams as BrowserPoolAcquireParams
137137
from .browser_pool_release_params import BrowserPoolReleaseParams as BrowserPoolReleaseParams
138+
from .browser_save_profile_params import BrowserSaveProfileParams as BrowserSaveProfileParams
138139
from .config_registry_list_params import ConfigRegistryListParams as ConfigRegistryListParams
139140
from .browser_network_config_param import BrowserNetworkConfigParam as BrowserNetworkConfigParam
140141
from .browser_network_update_param import BrowserNetworkUpdateParam as BrowserNetworkUpdateParam
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2+
3+
from __future__ import annotations
4+
5+
from typing_extensions import TypedDict
6+
7+
__all__ = ["BrowserSaveProfileParams"]
8+
9+
10+
class BrowserSaveProfileParams(TypedDict, total=False):
11+
id: str
12+
"""Existing profile ID to overwrite."""
13+
14+
name: str
15+
"""Profile name to overwrite or create."""

‎tests/api_resources/test_browsers.py‎

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from kernel import Kernel, AsyncKernel
1111
from tests.utils import assert_matches_type
1212
from kernel.types import (
13+
Profile,
1314
BrowserReplResult,
1415
BrowserCurlResponse,
1516
BrowserListResponse,
@@ -578,6 +579,58 @@ def test_path_params_repl(self, client: Kernel) -> None:
578579
code="code",
579580
)
580581

582+
@pytest.mark.skip(reason="Mock server tests are disabled")
583+
@parametrize
584+
def test_method_save_profile(self, client: Kernel) -> None:
585+
browser = client.browsers.save_profile(
586+
id_or_name="htzv5orfit78e1m2biiifpbv",
587+
)
588+
assert_matches_type(Profile, browser, path=["response"])
589+
590+
@pytest.mark.skip(reason="Mock server tests are disabled")
591+
@parametrize
592+
def test_method_save_profile_with_all_params(self, client: Kernel) -> None:
593+
browser = client.browsers.save_profile(
594+
id_or_name="htzv5orfit78e1m2biiifpbv",
595+
id="x",
596+
name="name",
597+
)
598+
assert_matches_type(Profile, browser, path=["response"])
599+
600+
@pytest.mark.skip(reason="Mock server tests are disabled")
601+
@parametrize
602+
def test_raw_response_save_profile(self, client: Kernel) -> None:
603+
response = client.browsers.with_raw_response.save_profile(
604+
id_or_name="htzv5orfit78e1m2biiifpbv",
605+
)
606+
607+
assert response.is_closed is True
608+
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
609+
browser = response.parse()
610+
assert_matches_type(Profile, browser, path=["response"])
611+
612+
@pytest.mark.skip(reason="Mock server tests are disabled")
613+
@parametrize
614+
def test_streaming_response_save_profile(self, client: Kernel) -> None:
615+
with client.browsers.with_streaming_response.save_profile(
616+
id_or_name="htzv5orfit78e1m2biiifpbv",
617+
) as response:
618+
assert not response.is_closed
619+
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
620+
621+
browser = response.parse()
622+
assert_matches_type(Profile, browser, path=["response"])
623+
624+
assert cast(Any, response.is_closed) is True
625+
626+
@pytest.mark.skip(reason="Mock server tests are disabled")
627+
@parametrize
628+
def test_path_params_save_profile(self, client: Kernel) -> None:
629+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
630+
client.browsers.with_raw_response.save_profile(
631+
id_or_name="",
632+
)
633+
581634

582635
class TestAsyncBrowsers:
583636
parametrize = pytest.mark.parametrize(
@@ -1136,3 +1189,55 @@ async def test_path_params_repl(self, async_client: AsyncKernel) -> None:
11361189
id_or_name="",
11371190
code="code",
11381191
)
1192+
1193+
@pytest.mark.skip(reason="Mock server tests are disabled")
1194+
@parametrize
1195+
async def test_method_save_profile(self, async_client: AsyncKernel) -> None:
1196+
browser = await async_client.browsers.save_profile(
1197+
id_or_name="htzv5orfit78e1m2biiifpbv",
1198+
)
1199+
assert_matches_type(Profile, browser, path=["response"])
1200+
1201+
@pytest.mark.skip(reason="Mock server tests are disabled")
1202+
@parametrize
1203+
async def test_method_save_profile_with_all_params(self, async_client: AsyncKernel) -> None:
1204+
browser = await async_client.browsers.save_profile(
1205+
id_or_name="htzv5orfit78e1m2biiifpbv",
1206+
id="x",
1207+
name="name",
1208+
)
1209+
assert_matches_type(Profile, browser, path=["response"])
1210+
1211+
@pytest.mark.skip(reason="Mock server tests are disabled")
1212+
@parametrize
1213+
async def test_raw_response_save_profile(self, async_client: AsyncKernel) -> None:
1214+
response = await async_client.browsers.with_raw_response.save_profile(
1215+
id_or_name="htzv5orfit78e1m2biiifpbv",
1216+
)
1217+
1218+
assert response.is_closed is True
1219+
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
1220+
browser = await response.parse()
1221+
assert_matches_type(Profile, browser, path=["response"])
1222+
1223+
@pytest.mark.skip(reason="Mock server tests are disabled")
1224+
@parametrize
1225+
async def test_streaming_response_save_profile(self, async_client: AsyncKernel) -> None:
1226+
async with async_client.browsers.with_streaming_response.save_profile(
1227+
id_or_name="htzv5orfit78e1m2biiifpbv",
1228+
) as response:
1229+
assert not response.is_closed
1230+
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
1231+
1232+
browser = await response.parse()
1233+
assert_matches_type(Profile, browser, path=["response"])
1234+
1235+
assert cast(Any, response.is_closed) is True
1236+
1237+
@pytest.mark.skip(reason="Mock server tests are disabled")
1238+
@parametrize
1239+
async def test_path_params_save_profile(self, async_client: AsyncKernel) -> None:
1240+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
1241+
await async_client.browsers.with_raw_response.save_profile(
1242+
id_or_name="",
1243+
)

0 commit comments

Comments
 (0)