From 6f16159d28de8129b12dfe2e144e9d1db39ad4ec Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:08:40 +0000 Subject: [PATCH 1/5] feat(api): surface deleted/expired API keys for audit trail (KERNEL-1350) --- .stats.yml | 4 +- api.md | 2 +- src/kernel/resources/api_keys.py | 36 ++++++++++++++++-- src/kernel/types/__init__.py | 1 + src/kernel/types/api_key.py | 15 ++++++++ src/kernel/types/api_key_list_params.py | 6 +++ src/kernel/types/api_key_retrieve_params.py | 15 ++++++++ tests/api_resources/test_api_keys.py | 41 ++++++++++++++++----- 8 files changed, 105 insertions(+), 15 deletions(-) create mode 100644 src/kernel/types/api_key_retrieve_params.py diff --git a/.stats.yml b/.stats.yml index a1640098..3ff52d98 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 119 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-51549f813f3002e18c6ca8d850cc0c7932828d511c151e0412c73b6798d19e30.yml -openapi_spec_hash: ee77b293c4bda91c1a32cfdd12b8739e +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-42074f2b600b0dc805377d6793e4bb30c959738b0f9cc44c409d094517e5e0ab.yml +openapi_spec_hash: 81c27a833d6d9637787634180dec2abd config_hash: 57567e00b41af47cef1b78e51b747aa0 diff --git a/api.md b/api.md index 4b24f102..d0bc3c26 100644 --- a/api.md +++ b/api.md @@ -449,7 +449,7 @@ from kernel.types import APIKey, CreatedAPIKey Methods: - client.api_keys.create(\*\*params) -> CreatedAPIKey -- client.api_keys.retrieve(id) -> APIKey +- client.api_keys.retrieve(id, \*\*params) -> APIKey - client.api_keys.update(id, \*\*params) -> APIKey - client.api_keys.list(\*\*params) -> SyncOffsetPagination[APIKey] - client.api_keys.delete(id) -> None diff --git a/src/kernel/resources/api_keys.py b/src/kernel/resources/api_keys.py index 93e80158..3801c4f1 100644 --- a/src/kernel/resources/api_keys.py +++ b/src/kernel/resources/api_keys.py @@ -7,7 +7,7 @@ import httpx -from ..types import api_key_list_params, api_key_create_params, api_key_update_params +from ..types import api_key_list_params, api_key_create_params, api_key_update_params, api_key_retrieve_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 @@ -99,6 +99,7 @@ def retrieve( self, id: str, *, + include_deleted: 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, @@ -112,6 +113,9 @@ def retrieve( masked. Args: + include_deleted: When true, return the API key even if it has been deleted (soft-deleted), for + audit purposes. Defaults to false, which returns 404 for a deleted key. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -125,7 +129,13 @@ def retrieve( return self._get( path_template("/org/api_keys/{id}", id=id), 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( + {"include_deleted": include_deleted}, api_key_retrieve_params.APIKeyRetrieveParams + ), ), cast_to=APIKey, ) @@ -170,6 +180,7 @@ def update( def list( self, *, + include_deleted: bool | Omit = omit, limit: int | Omit = omit, offset: int | Omit = omit, query: str | Omit = omit, @@ -187,6 +198,9 @@ def list( API keys are masked. Args: + include_deleted: When true, include deleted (soft-deleted) API keys in the results for audit + purposes. Defaults to false, which returns only live keys. + limit: Maximum number of results to return offset: Number of results to skip @@ -216,6 +230,7 @@ def list( timeout=timeout, query=maybe_transform( { + "include_deleted": include_deleted, "limit": limit, "offset": offset, "query": query, @@ -336,6 +351,7 @@ async def retrieve( self, id: str, *, + include_deleted: 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, @@ -349,6 +365,9 @@ async def retrieve( masked. Args: + include_deleted: When true, return the API key even if it has been deleted (soft-deleted), for + audit purposes. Defaults to false, which returns 404 for a deleted key. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -362,7 +381,13 @@ async def retrieve( return await self._get( path_template("/org/api_keys/{id}", id=id), 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( + {"include_deleted": include_deleted}, api_key_retrieve_params.APIKeyRetrieveParams + ), ), cast_to=APIKey, ) @@ -407,6 +432,7 @@ async def update( def list( self, *, + include_deleted: bool | Omit = omit, limit: int | Omit = omit, offset: int | Omit = omit, query: str | Omit = omit, @@ -424,6 +450,9 @@ def list( API keys are masked. Args: + include_deleted: When true, include deleted (soft-deleted) API keys in the results for audit + purposes. Defaults to false, which returns only live keys. + limit: Maximum number of results to return offset: Number of results to skip @@ -453,6 +482,7 @@ def list( timeout=timeout, query=maybe_transform( { + "include_deleted": include_deleted, "limit": limit, "offset": offset, "query": query, diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 1e9b39a4..d091f8de 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -54,6 +54,7 @@ from .deployment_state_event import DeploymentStateEvent as DeploymentStateEvent from .invocation_list_params import InvocationListParams as InvocationListParams from .invocation_state_event import InvocationStateEvent as InvocationStateEvent +from .api_key_retrieve_params import APIKeyRetrieveParams as APIKeyRetrieveParams from .browser_create_response import BrowserCreateResponse as BrowserCreateResponse from .browser_retrieve_params import BrowserRetrieveParams as BrowserRetrieveParams from .browser_update_response import BrowserUpdateResponse as BrowserUpdateResponse diff --git a/src/kernel/types/api_key.py b/src/kernel/types/api_key.py index 6df577f8..0f784c63 100644 --- a/src/kernel/types/api_key.py +++ b/src/kernel/types/api_key.py @@ -2,6 +2,7 @@ from typing import Optional from datetime import datetime +from typing_extensions import Literal from .._models import BaseModel @@ -28,6 +29,12 @@ class APIKey(BaseModel): created_by: CreatedBy + deleted_at: Optional[datetime] = None + """When the API key was deleted (soft-deleted). + + Null for keys that have not been deleted. + """ + expires_at: Optional[datetime] = None """When the API key expires""" @@ -45,3 +52,11 @@ class APIKey(BaseModel): Null means the key is org-wide or the project name is unavailable. """ + + status: Literal["active", "expired", "deleted"] + """Derived lifecycle status of the API key. + + `active` means usable. `expired` means past its expires_at. `deleted` means it + was deleted (soft-deleted) and can no longer authenticate. Deleted takes + precedence over expired. + """ diff --git a/src/kernel/types/api_key_list_params.py b/src/kernel/types/api_key_list_params.py index 79a9c41c..673c5d6d 100644 --- a/src/kernel/types/api_key_list_params.py +++ b/src/kernel/types/api_key_list_params.py @@ -8,6 +8,12 @@ class APIKeyListParams(TypedDict, total=False): + include_deleted: bool + """ + When true, include deleted (soft-deleted) API keys in the results for audit + purposes. Defaults to false, which returns only live keys. + """ + limit: int """Maximum number of results to return""" diff --git a/src/kernel/types/api_key_retrieve_params.py b/src/kernel/types/api_key_retrieve_params.py new file mode 100644 index 00000000..e2b9ea3b --- /dev/null +++ b/src/kernel/types/api_key_retrieve_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 TypedDict + +__all__ = ["APIKeyRetrieveParams"] + + +class APIKeyRetrieveParams(TypedDict, total=False): + include_deleted: bool + """ + When true, return the API key even if it has been deleted (soft-deleted), for + audit purposes. Defaults to false, which returns 404 for a deleted key. + """ diff --git a/tests/api_resources/test_api_keys.py b/tests/api_resources/test_api_keys.py index 9c3abcae..34273831 100644 --- a/tests/api_resources/test_api_keys.py +++ b/tests/api_resources/test_api_keys.py @@ -9,7 +9,10 @@ from kernel import Kernel, AsyncKernel from tests.utils import assert_matches_type -from kernel.types import APIKey, CreatedAPIKey +from kernel.types import ( + APIKey, + CreatedAPIKey, +) from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -66,7 +69,16 @@ def test_streaming_response_create(self, client: Kernel) -> None: @parametrize def test_method_retrieve(self, client: Kernel) -> None: api_key = client.api_keys.retrieve( - "id", + id="id", + ) + assert_matches_type(APIKey, api_key, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve_with_all_params(self, client: Kernel) -> None: + api_key = client.api_keys.retrieve( + id="id", + include_deleted=True, ) assert_matches_type(APIKey, api_key, path=["response"]) @@ -74,7 +86,7 @@ def test_method_retrieve(self, client: Kernel) -> None: @parametrize def test_raw_response_retrieve(self, client: Kernel) -> None: response = client.api_keys.with_raw_response.retrieve( - "id", + id="id", ) assert response.is_closed is True @@ -86,7 +98,7 @@ def test_raw_response_retrieve(self, client: Kernel) -> None: @parametrize def test_streaming_response_retrieve(self, client: Kernel) -> None: with client.api_keys.with_streaming_response.retrieve( - "id", + id="id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -101,7 +113,7 @@ def test_streaming_response_retrieve(self, client: Kernel) -> None: def test_path_params_retrieve(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): client.api_keys.with_raw_response.retrieve( - "", + id="", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -160,6 +172,7 @@ def test_method_list(self, client: Kernel) -> None: @parametrize def test_method_list_with_all_params(self, client: Kernel) -> None: api_key = client.api_keys.list( + include_deleted=True, limit=100, offset=0, query="query", @@ -286,7 +299,16 @@ async def test_streaming_response_create(self, async_client: AsyncKernel) -> Non @parametrize async def test_method_retrieve(self, async_client: AsyncKernel) -> None: api_key = await async_client.api_keys.retrieve( - "id", + id="id", + ) + assert_matches_type(APIKey, api_key, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve_with_all_params(self, async_client: AsyncKernel) -> None: + api_key = await async_client.api_keys.retrieve( + id="id", + include_deleted=True, ) assert_matches_type(APIKey, api_key, path=["response"]) @@ -294,7 +316,7 @@ async def test_method_retrieve(self, async_client: AsyncKernel) -> None: @parametrize async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: response = await async_client.api_keys.with_raw_response.retrieve( - "id", + id="id", ) assert response.is_closed is True @@ -306,7 +328,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: async with async_client.api_keys.with_streaming_response.retrieve( - "id", + id="id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -321,7 +343,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> N async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): await async_client.api_keys.with_raw_response.retrieve( - "", + id="", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -380,6 +402,7 @@ async def test_method_list(self, async_client: AsyncKernel) -> None: @parametrize async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: api_key = await async_client.api_keys.list( + include_deleted=True, limit=100, offset=0, query="query", From 1ba719ec1608a652d7a9e4510ac04b29a2bb1b79 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 12 Jun 2026 19:04:28 +0000 Subject: [PATCH 2/5] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 3ff52d98..ab902c2f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 119 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-42074f2b600b0dc805377d6793e4bb30c959738b0f9cc44c409d094517e5e0ab.yml -openapi_spec_hash: 81c27a833d6d9637787634180dec2abd +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-c98841235b0ece0591f28f7dd424339b6ef2f3e8f539b95b670ae0da2ef43df4.yml +openapi_spec_hash: c1e9456765f0743a333af297d135d5cf config_hash: 57567e00b41af47cef1b78e51b747aa0 From 5ad5ee511057a6bcaa69fee40087e3cf490a4494 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 14 Jun 2026 19:09:02 +0000 Subject: [PATCH 3/5] feat: Add API key rotate endpoint --- .stats.yml | 8 +- api.md | 1 + src/kernel/resources/api_keys.py | 122 +++++++++++++++++++++- src/kernel/types/__init__.py | 1 + src/kernel/types/api_key_rotate_params.py | 23 ++++ tests/api_resources/test_api_keys.py | 104 ++++++++++++++++++ 6 files changed, 254 insertions(+), 5 deletions(-) create mode 100644 src/kernel/types/api_key_rotate_params.py diff --git a/.stats.yml b/.stats.yml index ab902c2f..8d8fe88a 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 119 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-c98841235b0ece0591f28f7dd424339b6ef2f3e8f539b95b670ae0da2ef43df4.yml -openapi_spec_hash: c1e9456765f0743a333af297d135d5cf -config_hash: 57567e00b41af47cef1b78e51b747aa0 +configured_endpoints: 120 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-1f10598afa01d76d22b0ed63685248f482f74c9353cffe1d3e4a3d38da7716cf.yml +openapi_spec_hash: 8936f458bfa681b709e459ca1cc76fb5 +config_hash: 03c7e57f268c750e2415831662e95969 diff --git a/api.md b/api.md index d0bc3c26..8ba0dd44 100644 --- a/api.md +++ b/api.md @@ -453,6 +453,7 @@ Methods: - client.api_keys.update(id, \*\*params) -> APIKey - client.api_keys.list(\*\*params) -> SyncOffsetPagination[APIKey] - client.api_keys.delete(id) -> None +- client.api_keys.rotate(id, \*\*params) -> CreatedAPIKey # CredentialProviders diff --git a/src/kernel/resources/api_keys.py b/src/kernel/resources/api_keys.py index 3801c4f1..3c203dfd 100644 --- a/src/kernel/resources/api_keys.py +++ b/src/kernel/resources/api_keys.py @@ -7,7 +7,13 @@ import httpx -from ..types import api_key_list_params, api_key_create_params, api_key_update_params, api_key_retrieve_params +from ..types import ( + api_key_list_params, + api_key_create_params, + api_key_rotate_params, + api_key_update_params, + api_key_retrieve_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 @@ -277,6 +283,57 @@ def delete( cast_to=NoneType, ) + def rotate( + self, + id: str, + *, + days_to_expire: Optional[int] | Omit = omit, + expire_in_days: Optional[int] | 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, + ) -> CreatedAPIKey: + """Rotate an API key. + + Issues a new key that copies the name and project of the + rotated key, and schedules the rotated key to expire after a grace period so + in-flight callers can swap over. The new plaintext key is returned once. + + Args: + days_to_expire: Lifetime in days for the new key, up to 3650. Omit to reuse the rotated key's + original lifetime, or never-expires if it had none. + + expire_in_days: Grace period in days before the rotated key expires. Use 0 to expire it + immediately. Omit for the default grace period of 7 days. + + 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._post( + path_template("/org/api_keys/{id}/rotate", id=id), + body=maybe_transform( + { + "days_to_expire": days_to_expire, + "expire_in_days": expire_in_days, + }, + api_key_rotate_params.APIKeyRotateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CreatedAPIKey, + ) + class AsyncAPIKeysResource(AsyncAPIResource): """Create and manage API keys for organization and project-scoped access.""" @@ -529,6 +586,57 @@ async def delete( cast_to=NoneType, ) + async def rotate( + self, + id: str, + *, + days_to_expire: Optional[int] | Omit = omit, + expire_in_days: Optional[int] | 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, + ) -> CreatedAPIKey: + """Rotate an API key. + + Issues a new key that copies the name and project of the + rotated key, and schedules the rotated key to expire after a grace period so + in-flight callers can swap over. The new plaintext key is returned once. + + Args: + days_to_expire: Lifetime in days for the new key, up to 3650. Omit to reuse the rotated key's + original lifetime, or never-expires if it had none. + + expire_in_days: Grace period in days before the rotated key expires. Use 0 to expire it + immediately. Omit for the default grace period of 7 days. + + 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._post( + path_template("/org/api_keys/{id}/rotate", id=id), + body=await async_maybe_transform( + { + "days_to_expire": days_to_expire, + "expire_in_days": expire_in_days, + }, + api_key_rotate_params.APIKeyRotateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CreatedAPIKey, + ) + class APIKeysResourceWithRawResponse: def __init__(self, api_keys: APIKeysResource) -> None: @@ -549,6 +657,9 @@ def __init__(self, api_keys: APIKeysResource) -> None: self.delete = to_raw_response_wrapper( api_keys.delete, ) + self.rotate = to_raw_response_wrapper( + api_keys.rotate, + ) class AsyncAPIKeysResourceWithRawResponse: @@ -570,6 +681,9 @@ def __init__(self, api_keys: AsyncAPIKeysResource) -> None: self.delete = async_to_raw_response_wrapper( api_keys.delete, ) + self.rotate = async_to_raw_response_wrapper( + api_keys.rotate, + ) class APIKeysResourceWithStreamingResponse: @@ -591,6 +705,9 @@ def __init__(self, api_keys: APIKeysResource) -> None: self.delete = to_streamed_response_wrapper( api_keys.delete, ) + self.rotate = to_streamed_response_wrapper( + api_keys.rotate, + ) class AsyncAPIKeysResourceWithStreamingResponse: @@ -612,3 +729,6 @@ def __init__(self, api_keys: AsyncAPIKeysResource) -> None: self.delete = async_to_streamed_response_wrapper( api_keys.delete, ) + self.rotate = async_to_streamed_response_wrapper( + api_keys.rotate, + ) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index d091f8de..e38ba407 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -39,6 +39,7 @@ from .proxy_list_response import ProxyListResponse as ProxyListResponse from .proxy_check_response import ProxyCheckResponse as ProxyCheckResponse from .api_key_create_params import APIKeyCreateParams as APIKeyCreateParams +from .api_key_rotate_params import APIKeyRotateParams as APIKeyRotateParams from .api_key_update_params import APIKeyUpdateParams as APIKeyUpdateParams from .browser_create_params import BrowserCreateParams as BrowserCreateParams from .browser_curl_response import BrowserCurlResponse as BrowserCurlResponse diff --git a/src/kernel/types/api_key_rotate_params.py b/src/kernel/types/api_key_rotate_params.py new file mode 100644 index 00000000..3810e589 --- /dev/null +++ b/src/kernel/types/api_key_rotate_params.py @@ -0,0 +1,23 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import TypedDict + +__all__ = ["APIKeyRotateParams"] + + +class APIKeyRotateParams(TypedDict, total=False): + days_to_expire: Optional[int] + """Lifetime in days for the new key, up to 3650. + + Omit to reuse the rotated key's original lifetime, or never-expires if it had + none. + """ + + expire_in_days: Optional[int] + """Grace period in days before the rotated key expires. + + Use 0 to expire it immediately. Omit for the default grace period of 7 days. + """ diff --git a/tests/api_resources/test_api_keys.py b/tests/api_resources/test_api_keys.py index 34273831..9eb265fc 100644 --- a/tests/api_resources/test_api_keys.py +++ b/tests/api_resources/test_api_keys.py @@ -245,6 +245,58 @@ def test_path_params_delete(self, client: Kernel) -> None: "", ) + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_rotate(self, client: Kernel) -> None: + api_key = client.api_keys.rotate( + id="id", + ) + assert_matches_type(CreatedAPIKey, api_key, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_rotate_with_all_params(self, client: Kernel) -> None: + api_key = client.api_keys.rotate( + id="id", + days_to_expire=30, + expire_in_days=7, + ) + assert_matches_type(CreatedAPIKey, api_key, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_rotate(self, client: Kernel) -> None: + response = client.api_keys.with_raw_response.rotate( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + api_key = response.parse() + assert_matches_type(CreatedAPIKey, api_key, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_rotate(self, client: Kernel) -> None: + with client.api_keys.with_streaming_response.rotate( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + api_key = response.parse() + assert_matches_type(CreatedAPIKey, api_key, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_rotate(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.api_keys.with_raw_response.rotate( + id="", + ) + class TestAsyncAPIKeys: parametrize = pytest.mark.parametrize( @@ -474,3 +526,55 @@ async def test_path_params_delete(self, async_client: AsyncKernel) -> None: await async_client.api_keys.with_raw_response.delete( "", ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_rotate(self, async_client: AsyncKernel) -> None: + api_key = await async_client.api_keys.rotate( + id="id", + ) + assert_matches_type(CreatedAPIKey, api_key, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_rotate_with_all_params(self, async_client: AsyncKernel) -> None: + api_key = await async_client.api_keys.rotate( + id="id", + days_to_expire=30, + expire_in_days=7, + ) + assert_matches_type(CreatedAPIKey, api_key, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_rotate(self, async_client: AsyncKernel) -> None: + response = await async_client.api_keys.with_raw_response.rotate( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + api_key = await response.parse() + assert_matches_type(CreatedAPIKey, api_key, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_rotate(self, async_client: AsyncKernel) -> None: + async with async_client.api_keys.with_streaming_response.rotate( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + api_key = await response.parse() + assert_matches_type(CreatedAPIKey, api_key, 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_rotate(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.api_keys.with_raw_response.rotate( + id="", + ) From bdf7a0dec6ed448f4846186fba05e4cbb7eb0846 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 14 Jun 2026 20:21:20 +0000 Subject: [PATCH 4/5] refactor(api): align API key audit surface with browser sibling (KERNEL-1350) --- .stats.yml | 4 ++-- src/kernel/resources/api_keys.py | 20 ++++++++++++++++---- src/kernel/types/api_key.py | 9 --------- src/kernel/types/api_key_list_params.py | 13 +++++++++++-- tests/api_resources/test_api_keys.py | 2 ++ 5 files changed, 31 insertions(+), 17 deletions(-) diff --git a/.stats.yml b/.stats.yml index 8d8fe88a..4127b5f2 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 120 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-1f10598afa01d76d22b0ed63685248f482f74c9353cffe1d3e4a3d38da7716cf.yml -openapi_spec_hash: 8936f458bfa681b709e459ca1cc76fb5 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e8afdbeac9332cf79200c2eb873e532104fd0a7472b08e63cde6c857a87cf0c3.yml +openapi_spec_hash: 2525caf30dffbdd83c83948201f11a52 config_hash: 03c7e57f268c750e2415831662e95969 diff --git a/src/kernel/resources/api_keys.py b/src/kernel/resources/api_keys.py index 3c203dfd..da28774d 100644 --- a/src/kernel/resources/api_keys.py +++ b/src/kernel/resources/api_keys.py @@ -192,6 +192,7 @@ def list( query: str | Omit = omit, sort_by: Literal["created_at", "name", "expires_at"] | Omit = omit, sort_direction: Literal["asc", "desc"] | Omit = omit, + status: Literal["active", "deleted", "all"] | 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, @@ -204,8 +205,8 @@ def list( API keys are masked. Args: - include_deleted: When true, include deleted (soft-deleted) API keys in the results for audit - purposes. Defaults to false, which returns only live keys. + include_deleted: Deprecated: use status=all instead. When true, include deleted (soft-deleted) + API keys in the results for audit purposes. limit: Maximum number of results to return @@ -218,6 +219,10 @@ def list( sort_direction: Sort direction for API keys. + status: Filter API keys by status. "active" returns keys that are not deleted (default; + expired-but-not-deleted keys are still included), "deleted" returns only + soft-deleted keys, "all" returns both. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -242,6 +247,7 @@ def list( "query": query, "sort_by": sort_by, "sort_direction": sort_direction, + "status": status, }, api_key_list_params.APIKeyListParams, ), @@ -495,6 +501,7 @@ def list( query: str | Omit = omit, sort_by: Literal["created_at", "name", "expires_at"] | Omit = omit, sort_direction: Literal["asc", "desc"] | Omit = omit, + status: Literal["active", "deleted", "all"] | 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, @@ -507,8 +514,8 @@ def list( API keys are masked. Args: - include_deleted: When true, include deleted (soft-deleted) API keys in the results for audit - purposes. Defaults to false, which returns only live keys. + include_deleted: Deprecated: use status=all instead. When true, include deleted (soft-deleted) + API keys in the results for audit purposes. limit: Maximum number of results to return @@ -521,6 +528,10 @@ def list( sort_direction: Sort direction for API keys. + status: Filter API keys by status. "active" returns keys that are not deleted (default; + expired-but-not-deleted keys are still included), "deleted" returns only + soft-deleted keys, "all" returns both. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -545,6 +556,7 @@ def list( "query": query, "sort_by": sort_by, "sort_direction": sort_direction, + "status": status, }, api_key_list_params.APIKeyListParams, ), diff --git a/src/kernel/types/api_key.py b/src/kernel/types/api_key.py index 0f784c63..44519527 100644 --- a/src/kernel/types/api_key.py +++ b/src/kernel/types/api_key.py @@ -2,7 +2,6 @@ from typing import Optional from datetime import datetime -from typing_extensions import Literal from .._models import BaseModel @@ -52,11 +51,3 @@ class APIKey(BaseModel): Null means the key is org-wide or the project name is unavailable. """ - - status: Literal["active", "expired", "deleted"] - """Derived lifecycle status of the API key. - - `active` means usable. `expired` means past its expires_at. `deleted` means it - was deleted (soft-deleted) and can no longer authenticate. Deleted takes - precedence over expired. - """ diff --git a/src/kernel/types/api_key_list_params.py b/src/kernel/types/api_key_list_params.py index 673c5d6d..3ddc2a38 100644 --- a/src/kernel/types/api_key_list_params.py +++ b/src/kernel/types/api_key_list_params.py @@ -9,9 +9,10 @@ class APIKeyListParams(TypedDict, total=False): include_deleted: bool - """ + """Deprecated: use status=all instead. + When true, include deleted (soft-deleted) API keys in the results for audit - purposes. Defaults to false, which returns only live keys. + purposes. """ limit: int @@ -31,3 +32,11 @@ class APIKeyListParams(TypedDict, total=False): sort_direction: Literal["asc", "desc"] """Sort direction for API keys.""" + + status: Literal["active", "deleted", "all"] + """Filter API keys by status. + + "active" returns keys that are not deleted (default; expired-but-not-deleted + keys are still included), "deleted" returns only soft-deleted keys, "all" + returns both. + """ diff --git a/tests/api_resources/test_api_keys.py b/tests/api_resources/test_api_keys.py index 9eb265fc..5fdb5dc6 100644 --- a/tests/api_resources/test_api_keys.py +++ b/tests/api_resources/test_api_keys.py @@ -178,6 +178,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None: query="query", sort_by="created_at", sort_direction="asc", + status="active", ) assert_matches_type(SyncOffsetPagination[APIKey], api_key, path=["response"]) @@ -460,6 +461,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N query="query", sort_by="created_at", sort_direction="asc", + status="active", ) assert_matches_type(AsyncOffsetPagination[APIKey], api_key, path=["response"]) From ed30024c4ec7c5502e3f3d20dc246d36a788e39a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 14 Jun 2026 20:21:44 +0000 Subject: [PATCH 5/5] release: 0.68.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 14 ++++++++++++++ pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index db00cae0..0b8a7c68 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.67.0" + ".": "0.68.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 342bd471..746ed91f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 0.68.0 (2026-06-14) + +Full Changelog: [v0.67.0...v0.68.0](https://github.com/kernel/kernel-python-sdk/compare/v0.67.0...v0.68.0) + +### Features + +* Add API key rotate endpoint ([5ad5ee5](https://github.com/kernel/kernel-python-sdk/commit/5ad5ee511057a6bcaa69fee40087e3cf490a4494)) +* **api:** surface deleted/expired API keys for audit trail (KERNEL-1350) ([6f16159](https://github.com/kernel/kernel-python-sdk/commit/6f16159d28de8129b12dfe2e144e9d1db39ad4ec)) + + +### Refactors + +* **api:** align API key audit surface with browser sibling (KERNEL-1350) ([bdf7a0d](https://github.com/kernel/kernel-python-sdk/commit/bdf7a0dec6ed448f4846186fba05e4cbb7eb0846)) + ## 0.67.0 (2026-06-11) Full Changelog: [v0.66.0...v0.67.0](https://github.com/kernel/kernel-python-sdk/compare/v0.66.0...v0.67.0) diff --git a/pyproject.toml b/pyproject.toml index 035e174c..4020062f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.67.0" +version = "0.68.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 7c28de48..b0e37130 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.67.0" # x-release-please-version +__version__ = "0.68.0" # x-release-please-version