From cecdf6d4a4ef7919b6eef5d03f76542b6ade0906 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 22:04:53 +0000 Subject: [PATCH 1/4] feat: Count project names by Unicode code point --- .stats.yml | 4 ++-- src/kernel/resources/projects/projects.py | 8 ++++---- src/kernel/types/project_create_params.py | 2 +- src/kernel/types/project_update_params.py | 2 +- tests/api_resources/test_projects.py | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.stats.yml b/.stats.yml index c189c7a2..f5843501 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 127 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-1dffe057847b596dd311ce4b6e0e8036126dce3e97d390636c9c8e6d2f4823e1.yml -openapi_spec_hash: c06336c199ee64c222ba3122deae9463 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-6568ec20bad9bc5257619dae6b6817eedc308f8b74453912e660df95e329e44a.yml +openapi_spec_hash: 17b9ed1a0a3193731c52c4b1aac2b5b8 config_hash: 77ee715aa17061166f9a02b264a21b8d diff --git a/src/kernel/resources/projects/projects.py b/src/kernel/resources/projects/projects.py index c8ca08f2..ca3d6189 100644 --- a/src/kernel/resources/projects/projects.py +++ b/src/kernel/resources/projects/projects.py @@ -74,7 +74,7 @@ def create( Create a new project within the authenticated organization. Args: - name: Project name (1-255 characters) + name: Project name (1-255 Unicode code points) extra_headers: Send extra headers @@ -144,7 +144,7 @@ def update( Update a project's name or status. Args: - name: New project name + name: New project name (1-255 Unicode code points) status: New project status @@ -307,7 +307,7 @@ async def create( Create a new project within the authenticated organization. Args: - name: Project name (1-255 characters) + name: Project name (1-255 Unicode code points) extra_headers: Send extra headers @@ -377,7 +377,7 @@ async def update( Update a project's name or status. Args: - name: New project name + name: New project name (1-255 Unicode code points) status: New project status diff --git a/src/kernel/types/project_create_params.py b/src/kernel/types/project_create_params.py index 99a59864..9c51c570 100644 --- a/src/kernel/types/project_create_params.py +++ b/src/kernel/types/project_create_params.py @@ -9,4 +9,4 @@ class ProjectCreateParams(TypedDict, total=False): name: Required[str] - """Project name (1-255 characters)""" + """Project name (1-255 Unicode code points)""" diff --git a/src/kernel/types/project_update_params.py b/src/kernel/types/project_update_params.py index ea7de900..7b4359ff 100644 --- a/src/kernel/types/project_update_params.py +++ b/src/kernel/types/project_update_params.py @@ -9,7 +9,7 @@ class ProjectUpdateParams(TypedDict, total=False): name: str - """New project name""" + """New project name (1-255 Unicode code points)""" status: Literal["active", "archived"] """New project status""" diff --git a/tests/api_resources/test_projects.py b/tests/api_resources/test_projects.py index 007353fe..e8d98c29 100644 --- a/tests/api_resources/test_projects.py +++ b/tests/api_resources/test_projects.py @@ -107,7 +107,7 @@ def test_method_update(self, client: Kernel) -> None: def test_method_update_with_all_params(self, client: Kernel) -> None: project = client.projects.update( id="id", - name="name", + name="x", status="active", ) assert_matches_type(Project, project, path=["response"]) @@ -322,7 +322,7 @@ async def test_method_update(self, async_client: AsyncKernel) -> None: async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> None: project = await async_client.projects.update( id="id", - name="name", + name="x", status="active", ) assert_matches_type(Project, project, path=["response"]) From 3a2eb97686f3ecf02db16d7bccea803476cb730f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 19:39:19 +0000 Subject: [PATCH 2/4] feat: Stream profile downloads as tar archives --- .stats.yml | 4 +- api.md | 2 +- src/kernel/resources/profiles.py | 44 ++++++++++++++++---- src/kernel/types/__init__.py | 1 + src/kernel/types/profile_download_params.py | 15 +++++++ tests/api_resources/test_profiles.py | 46 +++++++++++++++++---- 6 files changed, 91 insertions(+), 21 deletions(-) create mode 100644 src/kernel/types/profile_download_params.py diff --git a/.stats.yml b/.stats.yml index f5843501..3dedd81c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 127 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-6568ec20bad9bc5257619dae6b6817eedc308f8b74453912e660df95e329e44a.yml -openapi_spec_hash: 17b9ed1a0a3193731c52c4b1aac2b5b8 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-5163e8941e42f7fb84e10f572f538202b31722c84bb980db74ccd44a960eb87f.yml +openapi_spec_hash: 8aa974cf7843681d6ae3e6dcf099596d config_hash: 77ee715aa17061166f9a02b264a21b8d diff --git a/api.md b/api.md index 7feea4cd..edc133e0 100644 --- a/api.md +++ b/api.md @@ -288,7 +288,7 @@ Methods: - client.profiles.update(id_or_name, \*\*params) -> Profile - client.profiles.list(\*\*params) -> SyncOffsetPagination[Profile] - client.profiles.delete(id_or_name) -> None -- client.profiles.download(id_or_name) -> BinaryAPIResponse +- client.profiles.download(id_or_name, \*\*params) -> BinaryAPIResponse # Auth diff --git a/src/kernel/resources/profiles.py b/src/kernel/resources/profiles.py index e804f559..836a9fb0 100644 --- a/src/kernel/resources/profiles.py +++ b/src/kernel/resources/profiles.py @@ -2,9 +2,11 @@ from __future__ import annotations +from typing_extensions import Literal + import httpx -from ..types import profile_list_params, profile_create_params, profile_update_params +from ..types import profile_list_params, profile_create_params, profile_update_params, profile_download_params from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property @@ -264,6 +266,7 @@ def download( self, id_or_name: str, *, + format: Literal["tar.zst", "tar"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -271,10 +274,17 @@ def download( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> BinaryAPIResponse: - """ - Returns a zstd-compressed tar file of the full user-data directory. + """Downloads the profile in its stored format by default. + + Current profiles are + returned as zstd-compressed tar archives, while legacy profiles remain JSON. Set + `format=tar` to decompress current profiles during download; legacy profiles + remain JSON. Args: + format: Response format for current profile archives. Legacy profiles are always + returned as JSON. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -285,11 +295,15 @@ def download( """ if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") - extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + extra_headers = {"Accept": "application/zstd", **(extra_headers or {})} return self._get( path_template("/profiles/{id_or_name}/download", id_or_name=id_or_name), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform({"format": format}, profile_download_params.ProfileDownloadParams), ), cast_to=BinaryAPIResponse, ) @@ -529,6 +543,7 @@ async def download( self, id_or_name: str, *, + format: Literal["tar.zst", "tar"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -536,10 +551,17 @@ async def download( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncBinaryAPIResponse: - """ - Returns a zstd-compressed tar file of the full user-data directory. + """Downloads the profile in its stored format by default. + + Current profiles are + returned as zstd-compressed tar archives, while legacy profiles remain JSON. Set + `format=tar` to decompress current profiles during download; legacy profiles + remain JSON. Args: + format: Response format for current profile archives. Legacy profiles are always + returned as JSON. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -550,11 +572,15 @@ async def download( """ if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") - extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + extra_headers = {"Accept": "application/zstd", **(extra_headers or {})} return await self._get( path_template("/profiles/{id_or_name}/download", id_or_name=id_or_name), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform({"format": format}, profile_download_params.ProfileDownloadParams), ), cast_to=AsyncBinaryAPIResponse, ) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 959865bf..ab58edbc 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -67,6 +67,7 @@ from .browser_update_response import BrowserUpdateResponse as BrowserUpdateResponse from .extension_list_response import ExtensionListResponse as ExtensionListResponse from .extension_upload_params import ExtensionUploadParams as ExtensionUploadParams +from .profile_download_params import ProfileDownloadParams as ProfileDownloadParams from .proxy_retrieve_response import ProxyRetrieveResponse as ProxyRetrieveResponse from .browser_pool_list_params import BrowserPoolListParams as BrowserPoolListParams from .credential_create_params import CredentialCreateParams as CredentialCreateParams diff --git a/src/kernel/types/profile_download_params.py b/src/kernel/types/profile_download_params.py new file mode 100644 index 00000000..32bfa3dd --- /dev/null +++ b/src/kernel/types/profile_download_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, TypedDict + +__all__ = ["ProfileDownloadParams"] + + +class ProfileDownloadParams(TypedDict, total=False): + format: Literal["tar.zst", "tar"] + """Response format for current profile archives. + + Legacy profiles are always returned as JSON. + """ diff --git a/tests/api_resources/test_profiles.py b/tests/api_resources/test_profiles.py index 683a3ce2..9317cf48 100644 --- a/tests/api_resources/test_profiles.py +++ b/tests/api_resources/test_profiles.py @@ -11,7 +11,9 @@ from kernel import Kernel, AsyncKernel from tests.utils import assert_matches_type -from kernel.types import Profile +from kernel.types import ( + Profile, +) from kernel._response import ( BinaryAPIResponse, AsyncBinaryAPIResponse, @@ -236,7 +238,20 @@ def test_path_params_delete(self, client: Kernel) -> None: def test_method_download(self, client: Kernel, respx_mock: MockRouter) -> None: respx_mock.get("/profiles/id_or_name/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) profile = client.profiles.download( - "id_or_name", + id_or_name="id_or_name", + ) + assert profile.is_closed + assert profile.json() == {"foo": "bar"} + assert cast(Any, profile.is_closed) is True + assert isinstance(profile, BinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_method_download_with_all_params(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/profiles/id_or_name/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + profile = client.profiles.download( + id_or_name="id_or_name", + format="tar.zst", ) assert profile.is_closed assert profile.json() == {"foo": "bar"} @@ -249,7 +264,7 @@ def test_raw_response_download(self, client: Kernel, respx_mock: MockRouter) -> respx_mock.get("/profiles/id_or_name/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) profile = client.profiles.with_raw_response.download( - "id_or_name", + id_or_name="id_or_name", ) assert profile.is_closed is True @@ -262,7 +277,7 @@ def test_raw_response_download(self, client: Kernel, respx_mock: MockRouter) -> def test_streaming_response_download(self, client: Kernel, respx_mock: MockRouter) -> None: respx_mock.get("/profiles/id_or_name/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) with client.profiles.with_streaming_response.download( - "id_or_name", + id_or_name="id_or_name", ) as profile: assert not profile.is_closed assert profile.http_request.headers.get("X-Stainless-Lang") == "python" @@ -278,7 +293,7 @@ def test_streaming_response_download(self, client: Kernel, respx_mock: MockRoute def test_path_params_download(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): client.profiles.with_raw_response.download( - "", + id_or_name="", ) @@ -497,7 +512,20 @@ async def test_path_params_delete(self, async_client: AsyncKernel) -> None: async def test_method_download(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: respx_mock.get("/profiles/id_or_name/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) profile = await async_client.profiles.download( - "id_or_name", + id_or_name="id_or_name", + ) + assert profile.is_closed + assert await profile.json() == {"foo": "bar"} + assert cast(Any, profile.is_closed) is True + assert isinstance(profile, AsyncBinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_method_download_with_all_params(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/profiles/id_or_name/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + profile = await async_client.profiles.download( + id_or_name="id_or_name", + format="tar.zst", ) assert profile.is_closed assert await profile.json() == {"foo": "bar"} @@ -510,7 +538,7 @@ async def test_raw_response_download(self, async_client: AsyncKernel, respx_mock respx_mock.get("/profiles/id_or_name/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) profile = await async_client.profiles.with_raw_response.download( - "id_or_name", + id_or_name="id_or_name", ) assert profile.is_closed is True @@ -523,7 +551,7 @@ async def test_raw_response_download(self, async_client: AsyncKernel, respx_mock async def test_streaming_response_download(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: respx_mock.get("/profiles/id_or_name/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) async with async_client.profiles.with_streaming_response.download( - "id_or_name", + id_or_name="id_or_name", ) as profile: assert not profile.is_closed assert profile.http_request.headers.get("X-Stainless-Lang") == "python" @@ -539,5 +567,5 @@ async def test_streaming_response_download(self, async_client: AsyncKernel, resp async def test_path_params_download(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): await async_client.profiles.with_raw_response.download( - "", + id_or_name="", ) From 95d1334660d2ebaed70d2170233e4bdf3b074958 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:16:38 +0000 Subject: [PATCH 3/4] feat: Use format-neutral profile download Accept header --- .stats.yml | 4 ++-- src/kernel/resources/profiles.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.stats.yml b/.stats.yml index 3dedd81c..c9122258 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 127 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-5163e8941e42f7fb84e10f572f538202b31722c84bb980db74ccd44a960eb87f.yml -openapi_spec_hash: 8aa974cf7843681d6ae3e6dcf099596d +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-477bd1ff6f7053f2bd4687d7c5fc9a6a1d56a584be42f927abc6fc858ff35718.yml +openapi_spec_hash: 3bf4cc824f6497c4ad109abd3187eede config_hash: 77ee715aa17061166f9a02b264a21b8d diff --git a/src/kernel/resources/profiles.py b/src/kernel/resources/profiles.py index 836a9fb0..8642940c 100644 --- a/src/kernel/resources/profiles.py +++ b/src/kernel/resources/profiles.py @@ -295,7 +295,7 @@ def download( """ if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") - extra_headers = {"Accept": "application/zstd", **(extra_headers or {})} + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} return self._get( path_template("/profiles/{id_or_name}/download", id_or_name=id_or_name), options=make_request_options( @@ -572,7 +572,7 @@ async def download( """ if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") - extra_headers = {"Accept": "application/zstd", **(extra_headers or {})} + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} return await self._get( path_template("/profiles/{id_or_name}/download", id_or_name=id_or_name), options=make_request_options( From 1e3b0c12f637ae20b01fc5051031220771ec4fc9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:17:04 +0000 Subject: [PATCH 4/4] release: 0.82.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 10 ++++++++++ pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index f8e21778..f24634e6 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.81.0" + ".": "0.82.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e652229..b2692c28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 0.82.0 (2026-07-23) + +Full Changelog: [v0.81.0...v0.82.0](https://github.com/kernel/kernel-python-sdk/compare/v0.81.0...v0.82.0) + +### Features + +* Count project names by Unicode code point ([cecdf6d](https://github.com/kernel/kernel-python-sdk/commit/cecdf6d4a4ef7919b6eef5d03f76542b6ade0906)) +* Stream profile downloads as tar archives ([3a2eb97](https://github.com/kernel/kernel-python-sdk/commit/3a2eb97686f3ecf02db16d7bccea803476cb730f)) +* Use format-neutral profile download Accept header ([95d1334](https://github.com/kernel/kernel-python-sdk/commit/95d1334660d2ebaed70d2170233e4bdf3b074958)) + ## 0.81.0 (2026-07-21) Full Changelog: [v0.80.0...v0.81.0](https://github.com/kernel/kernel-python-sdk/compare/v0.80.0...v0.81.0) diff --git a/pyproject.toml b/pyproject.toml index 3ecf5f86..f24b2fbe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.81.0" +version = "0.82.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 55bf102b..c0377ac4 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.81.0" # x-release-please-version +__version__ = "0.82.0" # x-release-please-version