Skip to content

Commit 6f16159

Browse files
feat(api): surface deleted/expired API keys for audit trail (KERNEL-1350)
1 parent 90cb376 commit 6f16159

8 files changed

Lines changed: 105 additions & 15 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: 119
2-
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-51549f813f3002e18c6ca8d850cc0c7932828d511c151e0412c73b6798d19e30.yml
3-
openapi_spec_hash: ee77b293c4bda91c1a32cfdd12b8739e
2+
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-42074f2b600b0dc805377d6793e4bb30c959738b0f9cc44c409d094517e5e0ab.yml
3+
openapi_spec_hash: 81c27a833d6d9637787634180dec2abd
44
config_hash: 57567e00b41af47cef1b78e51b747aa0

api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,7 @@ from kernel.types import APIKey, CreatedAPIKey
449449
Methods:
450450

451451
- <code title="post /org/api_keys">client.api_keys.<a href="./src/kernel/resources/api_keys.py">create</a>(\*\*<a href="src/kernel/types/api_key_create_params.py">params</a>) -> <a href="./src/kernel/types/created_api_key.py">CreatedAPIKey</a></code>
452-
- <code title="get /org/api_keys/{id}">client.api_keys.<a href="./src/kernel/resources/api_keys.py">retrieve</a>(id) -> <a href="./src/kernel/types/api_key.py">APIKey</a></code>
452+
- <code title="get /org/api_keys/{id}">client.api_keys.<a href="./src/kernel/resources/api_keys.py">retrieve</a>(id, \*\*<a href="src/kernel/types/api_key_retrieve_params.py">params</a>) -> <a href="./src/kernel/types/api_key.py">APIKey</a></code>
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>

src/kernel/resources/api_keys.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import httpx
99

10-
from ..types import api_key_list_params, api_key_create_params, api_key_update_params
10+
from ..types import api_key_list_params, api_key_create_params, api_key_update_params, api_key_retrieve_params
1111
from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
1212
from .._utils import path_template, maybe_transform, async_maybe_transform
1313
from .._compat import cached_property
@@ -99,6 +99,7 @@ def retrieve(
9999
self,
100100
id: str,
101101
*,
102+
include_deleted: bool | Omit = omit,
102103
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
103104
# The extra values given here take precedence over values defined on the client or passed to this method.
104105
extra_headers: Headers | None = None,
@@ -112,6 +113,9 @@ def retrieve(
112113
masked.
113114
114115
Args:
116+
include_deleted: When true, return the API key even if it has been deleted (soft-deleted), for
117+
audit purposes. Defaults to false, which returns 404 for a deleted key.
118+
115119
extra_headers: Send extra headers
116120
117121
extra_query: Add additional query parameters to the request
@@ -125,7 +129,13 @@ def retrieve(
125129
return self._get(
126130
path_template("/org/api_keys/{id}", id=id),
127131
options=make_request_options(
128-
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
132+
extra_headers=extra_headers,
133+
extra_query=extra_query,
134+
extra_body=extra_body,
135+
timeout=timeout,
136+
query=maybe_transform(
137+
{"include_deleted": include_deleted}, api_key_retrieve_params.APIKeyRetrieveParams
138+
),
129139
),
130140
cast_to=APIKey,
131141
)
@@ -170,6 +180,7 @@ def update(
170180
def list(
171181
self,
172182
*,
183+
include_deleted: bool | Omit = omit,
173184
limit: int | Omit = omit,
174185
offset: int | Omit = omit,
175186
query: str | Omit = omit,
@@ -187,6 +198,9 @@ def list(
187198
API keys are masked.
188199
189200
Args:
201+
include_deleted: When true, include deleted (soft-deleted) API keys in the results for audit
202+
purposes. Defaults to false, which returns only live keys.
203+
190204
limit: Maximum number of results to return
191205
192206
offset: Number of results to skip
@@ -216,6 +230,7 @@ def list(
216230
timeout=timeout,
217231
query=maybe_transform(
218232
{
233+
"include_deleted": include_deleted,
219234
"limit": limit,
220235
"offset": offset,
221236
"query": query,
@@ -336,6 +351,7 @@ async def retrieve(
336351
self,
337352
id: str,
338353
*,
354+
include_deleted: bool | Omit = omit,
339355
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
340356
# The extra values given here take precedence over values defined on the client or passed to this method.
341357
extra_headers: Headers | None = None,
@@ -349,6 +365,9 @@ async def retrieve(
349365
masked.
350366
351367
Args:
368+
include_deleted: When true, return the API key even if it has been deleted (soft-deleted), for
369+
audit purposes. Defaults to false, which returns 404 for a deleted key.
370+
352371
extra_headers: Send extra headers
353372
354373
extra_query: Add additional query parameters to the request
@@ -362,7 +381,13 @@ async def retrieve(
362381
return await self._get(
363382
path_template("/org/api_keys/{id}", id=id),
364383
options=make_request_options(
365-
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
384+
extra_headers=extra_headers,
385+
extra_query=extra_query,
386+
extra_body=extra_body,
387+
timeout=timeout,
388+
query=await async_maybe_transform(
389+
{"include_deleted": include_deleted}, api_key_retrieve_params.APIKeyRetrieveParams
390+
),
366391
),
367392
cast_to=APIKey,
368393
)
@@ -407,6 +432,7 @@ async def update(
407432
def list(
408433
self,
409434
*,
435+
include_deleted: bool | Omit = omit,
410436
limit: int | Omit = omit,
411437
offset: int | Omit = omit,
412438
query: str | Omit = omit,
@@ -424,6 +450,9 @@ def list(
424450
API keys are masked.
425451
426452
Args:
453+
include_deleted: When true, include deleted (soft-deleted) API keys in the results for audit
454+
purposes. Defaults to false, which returns only live keys.
455+
427456
limit: Maximum number of results to return
428457
429458
offset: Number of results to skip
@@ -453,6 +482,7 @@ def list(
453482
timeout=timeout,
454483
query=maybe_transform(
455484
{
485+
"include_deleted": include_deleted,
456486
"limit": limit,
457487
"offset": offset,
458488
"query": query,

src/kernel/types/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
from .deployment_state_event import DeploymentStateEvent as DeploymentStateEvent
5555
from .invocation_list_params import InvocationListParams as InvocationListParams
5656
from .invocation_state_event import InvocationStateEvent as InvocationStateEvent
57+
from .api_key_retrieve_params import APIKeyRetrieveParams as APIKeyRetrieveParams
5758
from .browser_create_response import BrowserCreateResponse as BrowserCreateResponse
5859
from .browser_retrieve_params import BrowserRetrieveParams as BrowserRetrieveParams
5960
from .browser_update_response import BrowserUpdateResponse as BrowserUpdateResponse

src/kernel/types/api_key.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from typing import Optional
44
from datetime import datetime
5+
from typing_extensions import Literal
56

67
from .._models import BaseModel
78

@@ -28,6 +29,12 @@ class APIKey(BaseModel):
2829

2930
created_by: CreatedBy
3031

32+
deleted_at: Optional[datetime] = None
33+
"""When the API key was deleted (soft-deleted).
34+
35+
Null for keys that have not been deleted.
36+
"""
37+
3138
expires_at: Optional[datetime] = None
3239
"""When the API key expires"""
3340

@@ -45,3 +52,11 @@ class APIKey(BaseModel):
4552
4653
Null means the key is org-wide or the project name is unavailable.
4754
"""
55+
56+
status: Literal["active", "expired", "deleted"]
57+
"""Derived lifecycle status of the API key.
58+
59+
`active` means usable. `expired` means past its expires_at. `deleted` means it
60+
was deleted (soft-deleted) and can no longer authenticate. Deleted takes
61+
precedence over expired.
62+
"""

src/kernel/types/api_key_list_params.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88

99

1010
class APIKeyListParams(TypedDict, total=False):
11+
include_deleted: bool
12+
"""
13+
When true, include deleted (soft-deleted) API keys in the results for audit
14+
purposes. Defaults to false, which returns only live keys.
15+
"""
16+
1117
limit: int
1218
"""Maximum number of results to return"""
1319

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 TypedDict
6+
7+
__all__ = ["APIKeyRetrieveParams"]
8+
9+
10+
class APIKeyRetrieveParams(TypedDict, total=False):
11+
include_deleted: bool
12+
"""
13+
When true, return the API key even if it has been deleted (soft-deleted), for
14+
audit purposes. Defaults to false, which returns 404 for a deleted key.
15+
"""

tests/api_resources/test_api_keys.py

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99

1010
from kernel import Kernel, AsyncKernel
1111
from tests.utils import assert_matches_type
12-
from kernel.types import APIKey, CreatedAPIKey
12+
from kernel.types import (
13+
APIKey,
14+
CreatedAPIKey,
15+
)
1316
from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination
1417

1518
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -66,15 +69,24 @@ def test_streaming_response_create(self, client: Kernel) -> None:
6669
@parametrize
6770
def test_method_retrieve(self, client: Kernel) -> None:
6871
api_key = client.api_keys.retrieve(
69-
"id",
72+
id="id",
73+
)
74+
assert_matches_type(APIKey, api_key, path=["response"])
75+
76+
@pytest.mark.skip(reason="Mock server tests are disabled")
77+
@parametrize
78+
def test_method_retrieve_with_all_params(self, client: Kernel) -> None:
79+
api_key = client.api_keys.retrieve(
80+
id="id",
81+
include_deleted=True,
7082
)
7183
assert_matches_type(APIKey, api_key, path=["response"])
7284

7385
@pytest.mark.skip(reason="Mock server tests are disabled")
7486
@parametrize
7587
def test_raw_response_retrieve(self, client: Kernel) -> None:
7688
response = client.api_keys.with_raw_response.retrieve(
77-
"id",
89+
id="id",
7890
)
7991

8092
assert response.is_closed is True
@@ -86,7 +98,7 @@ def test_raw_response_retrieve(self, client: Kernel) -> None:
8698
@parametrize
8799
def test_streaming_response_retrieve(self, client: Kernel) -> None:
88100
with client.api_keys.with_streaming_response.retrieve(
89-
"id",
101+
id="id",
90102
) as response:
91103
assert not response.is_closed
92104
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -101,7 +113,7 @@ def test_streaming_response_retrieve(self, client: Kernel) -> None:
101113
def test_path_params_retrieve(self, client: Kernel) -> None:
102114
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
103115
client.api_keys.with_raw_response.retrieve(
104-
"",
116+
id="",
105117
)
106118

107119
@pytest.mark.skip(reason="Mock server tests are disabled")
@@ -160,6 +172,7 @@ def test_method_list(self, client: Kernel) -> None:
160172
@parametrize
161173
def test_method_list_with_all_params(self, client: Kernel) -> None:
162174
api_key = client.api_keys.list(
175+
include_deleted=True,
163176
limit=100,
164177
offset=0,
165178
query="query",
@@ -286,15 +299,24 @@ async def test_streaming_response_create(self, async_client: AsyncKernel) -> Non
286299
@parametrize
287300
async def test_method_retrieve(self, async_client: AsyncKernel) -> None:
288301
api_key = await async_client.api_keys.retrieve(
289-
"id",
302+
id="id",
303+
)
304+
assert_matches_type(APIKey, api_key, path=["response"])
305+
306+
@pytest.mark.skip(reason="Mock server tests are disabled")
307+
@parametrize
308+
async def test_method_retrieve_with_all_params(self, async_client: AsyncKernel) -> None:
309+
api_key = await async_client.api_keys.retrieve(
310+
id="id",
311+
include_deleted=True,
290312
)
291313
assert_matches_type(APIKey, api_key, path=["response"])
292314

293315
@pytest.mark.skip(reason="Mock server tests are disabled")
294316
@parametrize
295317
async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None:
296318
response = await async_client.api_keys.with_raw_response.retrieve(
297-
"id",
319+
id="id",
298320
)
299321

300322
assert response.is_closed is True
@@ -306,7 +328,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None:
306328
@parametrize
307329
async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None:
308330
async with async_client.api_keys.with_streaming_response.retrieve(
309-
"id",
331+
id="id",
310332
) as response:
311333
assert not response.is_closed
312334
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
321343
async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None:
322344
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
323345
await async_client.api_keys.with_raw_response.retrieve(
324-
"",
346+
id="",
325347
)
326348

327349
@pytest.mark.skip(reason="Mock server tests are disabled")
@@ -380,6 +402,7 @@ async def test_method_list(self, async_client: AsyncKernel) -> None:
380402
@parametrize
381403
async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None:
382404
api_key = await async_client.api_keys.list(
405+
include_deleted=True,
383406
limit=100,
384407
offset=0,
385408
query="query",

0 commit comments

Comments
 (0)