Skip to content

Commit 3a2eb97

Browse files
feat: Stream profile downloads as tar archives
1 parent cecdf6d commit 3a2eb97

6 files changed

Lines changed: 91 additions & 21 deletions

File tree

.stats.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
configured_endpoints: 127
2-
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-6568ec20bad9bc5257619dae6b6817eedc308f8b74453912e660df95e329e44a.yml
3-
openapi_spec_hash: 17b9ed1a0a3193731c52c4b1aac2b5b8
2+
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-5163e8941e42f7fb84e10f572f538202b31722c84bb980db74ccd44a960eb87f.yml
3+
openapi_spec_hash: 8aa974cf7843681d6ae3e6dcf099596d
44
config_hash: 77ee715aa17061166f9a02b264a21b8d

api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ Methods:
288288
- <code title="patch /profiles/{id_or_name}">client.profiles.<a href="./src/kernel/resources/profiles.py">update</a>(id_or_name, \*\*<a href="src/kernel/types/profile_update_params.py">params</a>) -> <a href="./src/kernel/types/profile.py">Profile</a></code>
289289
- <code title="get /profiles">client.profiles.<a href="./src/kernel/resources/profiles.py">list</a>(\*\*<a href="src/kernel/types/profile_list_params.py">params</a>) -> <a href="./src/kernel/types/profile.py">SyncOffsetPagination[Profile]</a></code>
290290
- <code title="delete /profiles/{id_or_name}">client.profiles.<a href="./src/kernel/resources/profiles.py">delete</a>(id_or_name) -> None</code>
291-
- <code title="get /profiles/{id_or_name}/download">client.profiles.<a href="./src/kernel/resources/profiles.py">download</a>(id_or_name) -> BinaryAPIResponse</code>
291+
- <code title="get /profiles/{id_or_name}/download">client.profiles.<a href="./src/kernel/resources/profiles.py">download</a>(id_or_name, \*\*<a href="src/kernel/types/profile_download_params.py">params</a>) -> BinaryAPIResponse</code>
292292

293293
# Auth
294294

src/kernel/resources/profiles.py

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
from __future__ import annotations
44

5+
from typing_extensions import Literal
6+
57
import httpx
68

7-
from ..types import profile_list_params, profile_create_params, profile_update_params
9+
from ..types import profile_list_params, profile_create_params, profile_update_params, profile_download_params
810
from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
911
from .._utils import path_template, maybe_transform, async_maybe_transform
1012
from .._compat import cached_property
@@ -264,17 +266,25 @@ def download(
264266
self,
265267
id_or_name: str,
266268
*,
269+
format: Literal["tar.zst", "tar"] | Omit = omit,
267270
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
268271
# The extra values given here take precedence over values defined on the client or passed to this method.
269272
extra_headers: Headers | None = None,
270273
extra_query: Query | None = None,
271274
extra_body: Body | None = None,
272275
timeout: float | httpx.Timeout | None | NotGiven = not_given,
273276
) -> BinaryAPIResponse:
274-
"""
275-
Returns a zstd-compressed tar file of the full user-data directory.
277+
"""Downloads the profile in its stored format by default.
278+
279+
Current profiles are
280+
returned as zstd-compressed tar archives, while legacy profiles remain JSON. Set
281+
`format=tar` to decompress current profiles during download; legacy profiles
282+
remain JSON.
276283
277284
Args:
285+
format: Response format for current profile archives. Legacy profiles are always
286+
returned as JSON.
287+
278288
extra_headers: Send extra headers
279289
280290
extra_query: Add additional query parameters to the request
@@ -285,11 +295,15 @@ def download(
285295
"""
286296
if not id_or_name:
287297
raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
288-
extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})}
298+
extra_headers = {"Accept": "application/zstd", **(extra_headers or {})}
289299
return self._get(
290300
path_template("/profiles/{id_or_name}/download", id_or_name=id_or_name),
291301
options=make_request_options(
292-
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
302+
extra_headers=extra_headers,
303+
extra_query=extra_query,
304+
extra_body=extra_body,
305+
timeout=timeout,
306+
query=maybe_transform({"format": format}, profile_download_params.ProfileDownloadParams),
293307
),
294308
cast_to=BinaryAPIResponse,
295309
)
@@ -529,17 +543,25 @@ async def download(
529543
self,
530544
id_or_name: str,
531545
*,
546+
format: Literal["tar.zst", "tar"] | Omit = omit,
532547
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
533548
# The extra values given here take precedence over values defined on the client or passed to this method.
534549
extra_headers: Headers | None = None,
535550
extra_query: Query | None = None,
536551
extra_body: Body | None = None,
537552
timeout: float | httpx.Timeout | None | NotGiven = not_given,
538553
) -> AsyncBinaryAPIResponse:
539-
"""
540-
Returns a zstd-compressed tar file of the full user-data directory.
554+
"""Downloads the profile in its stored format by default.
555+
556+
Current profiles are
557+
returned as zstd-compressed tar archives, while legacy profiles remain JSON. Set
558+
`format=tar` to decompress current profiles during download; legacy profiles
559+
remain JSON.
541560
542561
Args:
562+
format: Response format for current profile archives. Legacy profiles are always
563+
returned as JSON.
564+
543565
extra_headers: Send extra headers
544566
545567
extra_query: Add additional query parameters to the request
@@ -550,11 +572,15 @@ async def download(
550572
"""
551573
if not id_or_name:
552574
raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
553-
extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})}
575+
extra_headers = {"Accept": "application/zstd", **(extra_headers or {})}
554576
return await self._get(
555577
path_template("/profiles/{id_or_name}/download", id_or_name=id_or_name),
556578
options=make_request_options(
557-
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
579+
extra_headers=extra_headers,
580+
extra_query=extra_query,
581+
extra_body=extra_body,
582+
timeout=timeout,
583+
query=await async_maybe_transform({"format": format}, profile_download_params.ProfileDownloadParams),
558584
),
559585
cast_to=AsyncBinaryAPIResponse,
560586
)

src/kernel/types/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
from .browser_update_response import BrowserUpdateResponse as BrowserUpdateResponse
6868
from .extension_list_response import ExtensionListResponse as ExtensionListResponse
6969
from .extension_upload_params import ExtensionUploadParams as ExtensionUploadParams
70+
from .profile_download_params import ProfileDownloadParams as ProfileDownloadParams
7071
from .proxy_retrieve_response import ProxyRetrieveResponse as ProxyRetrieveResponse
7172
from .browser_pool_list_params import BrowserPoolListParams as BrowserPoolListParams
7273
from .credential_create_params import CredentialCreateParams as CredentialCreateParams
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 Literal, TypedDict
6+
7+
__all__ = ["ProfileDownloadParams"]
8+
9+
10+
class ProfileDownloadParams(TypedDict, total=False):
11+
format: Literal["tar.zst", "tar"]
12+
"""Response format for current profile archives.
13+
14+
Legacy profiles are always returned as JSON.
15+
"""

tests/api_resources/test_profiles.py

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111

1212
from kernel import Kernel, AsyncKernel
1313
from tests.utils import assert_matches_type
14-
from kernel.types import Profile
14+
from kernel.types import (
15+
Profile,
16+
)
1517
from kernel._response import (
1618
BinaryAPIResponse,
1719
AsyncBinaryAPIResponse,
@@ -236,7 +238,20 @@ def test_path_params_delete(self, client: Kernel) -> None:
236238
def test_method_download(self, client: Kernel, respx_mock: MockRouter) -> None:
237239
respx_mock.get("/profiles/id_or_name/download").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
238240
profile = client.profiles.download(
239-
"id_or_name",
241+
id_or_name="id_or_name",
242+
)
243+
assert profile.is_closed
244+
assert profile.json() == {"foo": "bar"}
245+
assert cast(Any, profile.is_closed) is True
246+
assert isinstance(profile, BinaryAPIResponse)
247+
248+
@parametrize
249+
@pytest.mark.respx(base_url=base_url)
250+
def test_method_download_with_all_params(self, client: Kernel, respx_mock: MockRouter) -> None:
251+
respx_mock.get("/profiles/id_or_name/download").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
252+
profile = client.profiles.download(
253+
id_or_name="id_or_name",
254+
format="tar.zst",
240255
)
241256
assert profile.is_closed
242257
assert profile.json() == {"foo": "bar"}
@@ -249,7 +264,7 @@ def test_raw_response_download(self, client: Kernel, respx_mock: MockRouter) ->
249264
respx_mock.get("/profiles/id_or_name/download").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
250265

251266
profile = client.profiles.with_raw_response.download(
252-
"id_or_name",
267+
id_or_name="id_or_name",
253268
)
254269

255270
assert profile.is_closed is True
@@ -262,7 +277,7 @@ def test_raw_response_download(self, client: Kernel, respx_mock: MockRouter) ->
262277
def test_streaming_response_download(self, client: Kernel, respx_mock: MockRouter) -> None:
263278
respx_mock.get("/profiles/id_or_name/download").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
264279
with client.profiles.with_streaming_response.download(
265-
"id_or_name",
280+
id_or_name="id_or_name",
266281
) as profile:
267282
assert not profile.is_closed
268283
assert profile.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -278,7 +293,7 @@ def test_streaming_response_download(self, client: Kernel, respx_mock: MockRoute
278293
def test_path_params_download(self, client: Kernel) -> None:
279294
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
280295
client.profiles.with_raw_response.download(
281-
"",
296+
id_or_name="",
282297
)
283298

284299

@@ -497,7 +512,20 @@ async def test_path_params_delete(self, async_client: AsyncKernel) -> None:
497512
async def test_method_download(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None:
498513
respx_mock.get("/profiles/id_or_name/download").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
499514
profile = await async_client.profiles.download(
500-
"id_or_name",
515+
id_or_name="id_or_name",
516+
)
517+
assert profile.is_closed
518+
assert await profile.json() == {"foo": "bar"}
519+
assert cast(Any, profile.is_closed) is True
520+
assert isinstance(profile, AsyncBinaryAPIResponse)
521+
522+
@parametrize
523+
@pytest.mark.respx(base_url=base_url)
524+
async def test_method_download_with_all_params(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None:
525+
respx_mock.get("/profiles/id_or_name/download").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
526+
profile = await async_client.profiles.download(
527+
id_or_name="id_or_name",
528+
format="tar.zst",
501529
)
502530
assert profile.is_closed
503531
assert await profile.json() == {"foo": "bar"}
@@ -510,7 +538,7 @@ async def test_raw_response_download(self, async_client: AsyncKernel, respx_mock
510538
respx_mock.get("/profiles/id_or_name/download").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
511539

512540
profile = await async_client.profiles.with_raw_response.download(
513-
"id_or_name",
541+
id_or_name="id_or_name",
514542
)
515543

516544
assert profile.is_closed is True
@@ -523,7 +551,7 @@ async def test_raw_response_download(self, async_client: AsyncKernel, respx_mock
523551
async def test_streaming_response_download(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None:
524552
respx_mock.get("/profiles/id_or_name/download").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
525553
async with async_client.profiles.with_streaming_response.download(
526-
"id_or_name",
554+
id_or_name="id_or_name",
527555
) as profile:
528556
assert not profile.is_closed
529557
assert profile.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -539,5 +567,5 @@ async def test_streaming_response_download(self, async_client: AsyncKernel, resp
539567
async def test_path_params_download(self, async_client: AsyncKernel) -> None:
540568
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
541569
await async_client.profiles.with_raw_response.download(
542-
"",
570+
id_or_name="",
543571
)

0 commit comments

Comments
 (0)