Skip to content

Commit b5cff48

Browse files
feat: Add API key rotate endpoint
1 parent 6ae9965 commit b5cff48

6 files changed

Lines changed: 254 additions & 5 deletions

File tree

.stats.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
configured_endpoints: 119
2-
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-c98841235b0ece0591f28f7dd424339b6ef2f3e8f539b95b670ae0da2ef43df4.yml
3-
openapi_spec_hash: c1e9456765f0743a333af297d135d5cf
4-
config_hash: 57567e00b41af47cef1b78e51b747aa0
1+
configured_endpoints: 120
2+
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-1f10598afa01d76d22b0ed63685248f482f74c9353cffe1d3e4a3d38da7716cf.yml
3+
openapi_spec_hash: 8936f458bfa681b709e459ca1cc76fb5
4+
config_hash: 03c7e57f268c750e2415831662e95969

api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,7 @@ Methods:
453453
- <code title="patch /org/api_keys/{id}">client.api_keys.<a href="./src/kernel/resources/api_keys.py">update</a>(id, \*\*<a href="src/kernel/types/api_key_update_params.py">params</a>) -> <a href="./src/kernel/types/api_key.py">APIKey</a></code>
454454
- <code title="get /org/api_keys">client.api_keys.<a href="./src/kernel/resources/api_keys.py">list</a>(\*\*<a href="src/kernel/types/api_key_list_params.py">params</a>) -> <a href="./src/kernel/types/api_key.py">SyncOffsetPagination[APIKey]</a></code>
455455
- <code title="delete /org/api_keys/{id}">client.api_keys.<a href="./src/kernel/resources/api_keys.py">delete</a>(id) -> None</code>
456+
- <code title="post /org/api_keys/{id}/rotate">client.api_keys.<a href="./src/kernel/resources/api_keys.py">rotate</a>(id, \*\*<a href="src/kernel/types/api_key_rotate_params.py">params</a>) -> <a href="./src/kernel/types/created_api_key.py">CreatedAPIKey</a></code>
456457

457458
# CredentialProviders
458459

src/kernel/resources/api_keys.py

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@
77

88
import httpx
99

10-
from ..types import api_key_list_params, api_key_create_params, api_key_update_params, api_key_retrieve_params
10+
from ..types import (
11+
api_key_list_params,
12+
api_key_create_params,
13+
api_key_rotate_params,
14+
api_key_update_params,
15+
api_key_retrieve_params,
16+
)
1117
from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
1218
from .._utils import path_template, maybe_transform, async_maybe_transform
1319
from .._compat import cached_property
@@ -277,6 +283,57 @@ def delete(
277283
cast_to=NoneType,
278284
)
279285

286+
def rotate(
287+
self,
288+
id: str,
289+
*,
290+
days_to_expire: Optional[int] | Omit = omit,
291+
expire_in_days: Optional[int] | Omit = omit,
292+
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
293+
# The extra values given here take precedence over values defined on the client or passed to this method.
294+
extra_headers: Headers | None = None,
295+
extra_query: Query | None = None,
296+
extra_body: Body | None = None,
297+
timeout: float | httpx.Timeout | None | NotGiven = not_given,
298+
) -> CreatedAPIKey:
299+
"""Rotate an API key.
300+
301+
Issues a new key that copies the name and project of the
302+
rotated key, and schedules the rotated key to expire after a grace period so
303+
in-flight callers can swap over. The new plaintext key is returned once.
304+
305+
Args:
306+
days_to_expire: Lifetime in days for the new key, up to 3650. Omit to reuse the rotated key's
307+
original lifetime, or never-expires if it had none.
308+
309+
expire_in_days: Grace period in days before the rotated key expires. Use 0 to expire it
310+
immediately. Omit for the default grace period of 7 days.
311+
312+
extra_headers: Send extra headers
313+
314+
extra_query: Add additional query parameters to the request
315+
316+
extra_body: Add additional JSON properties to the request
317+
318+
timeout: Override the client-level default timeout for this request, in seconds
319+
"""
320+
if not id:
321+
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
322+
return self._post(
323+
path_template("/org/api_keys/{id}/rotate", id=id),
324+
body=maybe_transform(
325+
{
326+
"days_to_expire": days_to_expire,
327+
"expire_in_days": expire_in_days,
328+
},
329+
api_key_rotate_params.APIKeyRotateParams,
330+
),
331+
options=make_request_options(
332+
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
333+
),
334+
cast_to=CreatedAPIKey,
335+
)
336+
280337

281338
class AsyncAPIKeysResource(AsyncAPIResource):
282339
"""Create and manage API keys for organization and project-scoped access."""
@@ -529,6 +586,57 @@ async def delete(
529586
cast_to=NoneType,
530587
)
531588

589+
async def rotate(
590+
self,
591+
id: str,
592+
*,
593+
days_to_expire: Optional[int] | Omit = omit,
594+
expire_in_days: Optional[int] | Omit = omit,
595+
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
596+
# The extra values given here take precedence over values defined on the client or passed to this method.
597+
extra_headers: Headers | None = None,
598+
extra_query: Query | None = None,
599+
extra_body: Body | None = None,
600+
timeout: float | httpx.Timeout | None | NotGiven = not_given,
601+
) -> CreatedAPIKey:
602+
"""Rotate an API key.
603+
604+
Issues a new key that copies the name and project of the
605+
rotated key, and schedules the rotated key to expire after a grace period so
606+
in-flight callers can swap over. The new plaintext key is returned once.
607+
608+
Args:
609+
days_to_expire: Lifetime in days for the new key, up to 3650. Omit to reuse the rotated key's
610+
original lifetime, or never-expires if it had none.
611+
612+
expire_in_days: Grace period in days before the rotated key expires. Use 0 to expire it
613+
immediately. Omit for the default grace period of 7 days.
614+
615+
extra_headers: Send extra headers
616+
617+
extra_query: Add additional query parameters to the request
618+
619+
extra_body: Add additional JSON properties to the request
620+
621+
timeout: Override the client-level default timeout for this request, in seconds
622+
"""
623+
if not id:
624+
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
625+
return await self._post(
626+
path_template("/org/api_keys/{id}/rotate", id=id),
627+
body=await async_maybe_transform(
628+
{
629+
"days_to_expire": days_to_expire,
630+
"expire_in_days": expire_in_days,
631+
},
632+
api_key_rotate_params.APIKeyRotateParams,
633+
),
634+
options=make_request_options(
635+
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
636+
),
637+
cast_to=CreatedAPIKey,
638+
)
639+
532640

533641
class APIKeysResourceWithRawResponse:
534642
def __init__(self, api_keys: APIKeysResource) -> None:
@@ -549,6 +657,9 @@ def __init__(self, api_keys: APIKeysResource) -> None:
549657
self.delete = to_raw_response_wrapper(
550658
api_keys.delete,
551659
)
660+
self.rotate = to_raw_response_wrapper(
661+
api_keys.rotate,
662+
)
552663

553664

554665
class AsyncAPIKeysResourceWithRawResponse:
@@ -570,6 +681,9 @@ def __init__(self, api_keys: AsyncAPIKeysResource) -> None:
570681
self.delete = async_to_raw_response_wrapper(
571682
api_keys.delete,
572683
)
684+
self.rotate = async_to_raw_response_wrapper(
685+
api_keys.rotate,
686+
)
573687

574688

575689
class APIKeysResourceWithStreamingResponse:
@@ -591,6 +705,9 @@ def __init__(self, api_keys: APIKeysResource) -> None:
591705
self.delete = to_streamed_response_wrapper(
592706
api_keys.delete,
593707
)
708+
self.rotate = to_streamed_response_wrapper(
709+
api_keys.rotate,
710+
)
594711

595712

596713
class AsyncAPIKeysResourceWithStreamingResponse:
@@ -612,3 +729,6 @@ def __init__(self, api_keys: AsyncAPIKeysResource) -> None:
612729
self.delete = async_to_streamed_response_wrapper(
613730
api_keys.delete,
614731
)
732+
self.rotate = async_to_streamed_response_wrapper(
733+
api_keys.rotate,
734+
)

src/kernel/types/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from .proxy_list_response import ProxyListResponse as ProxyListResponse
4040
from .proxy_check_response import ProxyCheckResponse as ProxyCheckResponse
4141
from .api_key_create_params import APIKeyCreateParams as APIKeyCreateParams
42+
from .api_key_rotate_params import APIKeyRotateParams as APIKeyRotateParams
4243
from .api_key_update_params import APIKeyUpdateParams as APIKeyUpdateParams
4344
from .browser_create_params import BrowserCreateParams as BrowserCreateParams
4445
from .browser_curl_response import BrowserCurlResponse as BrowserCurlResponse
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2+
3+
from __future__ import annotations
4+
5+
from typing import Optional
6+
from typing_extensions import TypedDict
7+
8+
__all__ = ["APIKeyRotateParams"]
9+
10+
11+
class APIKeyRotateParams(TypedDict, total=False):
12+
days_to_expire: Optional[int]
13+
"""Lifetime in days for the new key, up to 3650.
14+
15+
Omit to reuse the rotated key's original lifetime, or never-expires if it had
16+
none.
17+
"""
18+
19+
expire_in_days: Optional[int]
20+
"""Grace period in days before the rotated key expires.
21+
22+
Use 0 to expire it immediately. Omit for the default grace period of 7 days.
23+
"""

tests/api_resources/test_api_keys.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,58 @@ def test_path_params_delete(self, client: Kernel) -> None:
245245
"",
246246
)
247247

248+
@pytest.mark.skip(reason="Mock server tests are disabled")
249+
@parametrize
250+
def test_method_rotate(self, client: Kernel) -> None:
251+
api_key = client.api_keys.rotate(
252+
id="id",
253+
)
254+
assert_matches_type(CreatedAPIKey, api_key, path=["response"])
255+
256+
@pytest.mark.skip(reason="Mock server tests are disabled")
257+
@parametrize
258+
def test_method_rotate_with_all_params(self, client: Kernel) -> None:
259+
api_key = client.api_keys.rotate(
260+
id="id",
261+
days_to_expire=30,
262+
expire_in_days=7,
263+
)
264+
assert_matches_type(CreatedAPIKey, api_key, path=["response"])
265+
266+
@pytest.mark.skip(reason="Mock server tests are disabled")
267+
@parametrize
268+
def test_raw_response_rotate(self, client: Kernel) -> None:
269+
response = client.api_keys.with_raw_response.rotate(
270+
id="id",
271+
)
272+
273+
assert response.is_closed is True
274+
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
275+
api_key = response.parse()
276+
assert_matches_type(CreatedAPIKey, api_key, path=["response"])
277+
278+
@pytest.mark.skip(reason="Mock server tests are disabled")
279+
@parametrize
280+
def test_streaming_response_rotate(self, client: Kernel) -> None:
281+
with client.api_keys.with_streaming_response.rotate(
282+
id="id",
283+
) as response:
284+
assert not response.is_closed
285+
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
286+
287+
api_key = response.parse()
288+
assert_matches_type(CreatedAPIKey, api_key, path=["response"])
289+
290+
assert cast(Any, response.is_closed) is True
291+
292+
@pytest.mark.skip(reason="Mock server tests are disabled")
293+
@parametrize
294+
def test_path_params_rotate(self, client: Kernel) -> None:
295+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
296+
client.api_keys.with_raw_response.rotate(
297+
id="",
298+
)
299+
248300

249301
class TestAsyncAPIKeys:
250302
parametrize = pytest.mark.parametrize(
@@ -474,3 +526,55 @@ async def test_path_params_delete(self, async_client: AsyncKernel) -> None:
474526
await async_client.api_keys.with_raw_response.delete(
475527
"",
476528
)
529+
530+
@pytest.mark.skip(reason="Mock server tests are disabled")
531+
@parametrize
532+
async def test_method_rotate(self, async_client: AsyncKernel) -> None:
533+
api_key = await async_client.api_keys.rotate(
534+
id="id",
535+
)
536+
assert_matches_type(CreatedAPIKey, api_key, path=["response"])
537+
538+
@pytest.mark.skip(reason="Mock server tests are disabled")
539+
@parametrize
540+
async def test_method_rotate_with_all_params(self, async_client: AsyncKernel) -> None:
541+
api_key = await async_client.api_keys.rotate(
542+
id="id",
543+
days_to_expire=30,
544+
expire_in_days=7,
545+
)
546+
assert_matches_type(CreatedAPIKey, api_key, path=["response"])
547+
548+
@pytest.mark.skip(reason="Mock server tests are disabled")
549+
@parametrize
550+
async def test_raw_response_rotate(self, async_client: AsyncKernel) -> None:
551+
response = await async_client.api_keys.with_raw_response.rotate(
552+
id="id",
553+
)
554+
555+
assert response.is_closed is True
556+
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
557+
api_key = await response.parse()
558+
assert_matches_type(CreatedAPIKey, api_key, path=["response"])
559+
560+
@pytest.mark.skip(reason="Mock server tests are disabled")
561+
@parametrize
562+
async def test_streaming_response_rotate(self, async_client: AsyncKernel) -> None:
563+
async with async_client.api_keys.with_streaming_response.rotate(
564+
id="id",
565+
) as response:
566+
assert not response.is_closed
567+
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
568+
569+
api_key = await response.parse()
570+
assert_matches_type(CreatedAPIKey, api_key, path=["response"])
571+
572+
assert cast(Any, response.is_closed) is True
573+
574+
@pytest.mark.skip(reason="Mock server tests are disabled")
575+
@parametrize
576+
async def test_path_params_rotate(self, async_client: AsyncKernel) -> None:
577+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
578+
await async_client.api_keys.with_raw_response.rotate(
579+
id="",
580+
)

0 commit comments

Comments
 (0)