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/.stats.yml b/.stats.yml
index c189c7a2..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-1dffe057847b596dd311ce4b6e0e8036126dce3e97d390636c9c8e6d2f4823e1.yml
-openapi_spec_hash: c06336c199ee64c222ba3122deae9463
+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/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/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/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
diff --git a/src/kernel/resources/profiles.py b/src/kernel/resources/profiles.py
index e804f559..8642940c 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
@@ -289,7 +299,11 @@ def download(
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
@@ -554,7 +576,11 @@ async def download(
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/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/__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/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_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="",
)
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"])