diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 6f257c8..f910f1a 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.72.0" + ".": "0.73.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 10d9022..9118082 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 124 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-4c243ff089133bd49322d98a6943647589972f71ecadc993fe9e5029972b3995.yml -openapi_spec_hash: a2cb637a19a070d07a1a4343c75444ee -config_hash: fb167e754ebb3a14649463725891c9d0 +configured_endpoints: 125 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-55409ab573762d8bc010fb34c885651ca858a97d4353b4776b7aafeaaa313257.yml +openapi_spec_hash: 0cf678d80f2a2b73fb9ec82d05c8cc0a +config_hash: 06186eb40e0058a2a87ac251fc07415d diff --git a/CHANGELOG.md b/CHANGELOG.md index ab94a97..238e659 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.73.0 (2026-07-01) + +Full Changelog: [v0.72.0...v0.73.0](https://github.com/kernel/kernel-python-sdk/compare/v0.72.0...v0.73.0) + +### Features + +* Add hidden audit-logs export endpoint ([619c672](https://github.com/kernel/kernel-python-sdk/commit/619c6720f986b835393793896bb5ad4b64f8673f)) + ## 0.72.0 (2026-06-26) Full Changelog: [v0.71.0...v0.72.0](https://github.com/kernel/kernel-python-sdk/compare/v0.71.0...v0.72.0) diff --git a/api.md b/api.md index 5c1c6b9..300e937 100644 --- a/api.md +++ b/api.md @@ -454,6 +454,7 @@ from kernel.types import AuditLogEntry Methods: - client.audit_logs.list(\*\*params) -> SyncPageTokenPagination[AuditLogEntry] +- client.audit_logs.export_chunk(\*\*params) -> BinaryAPIResponse # APIKeys diff --git a/pyproject.toml b/pyproject.toml index 1649ada..8188c8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.72.0" +version = "0.73.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 4584cbe..0bd7060 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.72.0" # x-release-please-version +__version__ = "0.73.0" # x-release-please-version diff --git a/src/kernel/resources/audit_logs.py b/src/kernel/resources/audit_logs.py index bce8b71..814db9c 100644 --- a/src/kernel/resources/audit_logs.py +++ b/src/kernel/resources/audit_logs.py @@ -4,19 +4,28 @@ from typing import Union from datetime import datetime +from typing_extensions import Literal import httpx -from ..types import audit_log_list_params +from ..types import audit_log_list_params, audit_log_export_chunk_params from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given -from .._utils import maybe_transform +from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, to_raw_response_wrapper, to_streamed_response_wrapper, async_to_raw_response_wrapper, + to_custom_raw_response_wrapper, async_to_streamed_response_wrapper, + to_custom_streamed_response_wrapper, + async_to_custom_raw_response_wrapper, + async_to_custom_streamed_response_wrapper, ) from ..pagination import SyncPageTokenPagination, AsyncPageTokenPagination from .._base_client import AsyncPaginator, make_request_options @@ -128,6 +137,91 @@ def list( model=AuditLogEntry, ) + def export_chunk( + self, + *, + end: Union[str, datetime], + start: Union[str, datetime], + auth_strategy: str | Omit = omit, + cursor: str | Omit = omit, + exclude_method: str | Omit = omit, + format: Literal["jsonl", "jsonl.gz"] | Omit = omit, + limit: int | Omit = omit, + method: str | Omit = omit, + search: str | Omit = omit, + search_user_id: SequenceNotStr[str] | Omit = omit, + service: str | 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, + ) -> BinaryAPIResponse: + """ + Download an organization's audit log records for a time range as a file, for + archival, compliance, or offline analysis. For interactive browsing, use GET + /audit-logs. + + Args: + end: Upper bound (exclusive) for the audit record timestamp. + + start: Lower bound (inclusive) for the audit record timestamp. + + auth_strategy: Filter by authentication strategy. + + cursor: Opaque cursor from X-Next-Cursor for the next chunk of older records. + + exclude_method: Filter out results by HTTP method. + + format: Encoding for the returned chunk. + + limit: Maximum number of records to return in this chunk. + + method: Filter by HTTP method. + + search: Free-text search over path, user ID, email, client IP, and status. + + search_user_id: Additional user IDs to OR into free-text search. + + service: Filter by service name. + + 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 + """ + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + return self._get( + "/audit-logs/export/chunk", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "end": end, + "start": start, + "auth_strategy": auth_strategy, + "cursor": cursor, + "exclude_method": exclude_method, + "format": format, + "limit": limit, + "method": method, + "search": search, + "search_user_id": search_user_id, + "service": service, + }, + audit_log_export_chunk_params.AuditLogExportChunkParams, + ), + ), + cast_to=BinaryAPIResponse, + ) + class AsyncAuditLogsResource(AsyncAPIResource): """Read audit log records for the authenticated organization.""" @@ -232,6 +326,91 @@ def list( model=AuditLogEntry, ) + async def export_chunk( + self, + *, + end: Union[str, datetime], + start: Union[str, datetime], + auth_strategy: str | Omit = omit, + cursor: str | Omit = omit, + exclude_method: str | Omit = omit, + format: Literal["jsonl", "jsonl.gz"] | Omit = omit, + limit: int | Omit = omit, + method: str | Omit = omit, + search: str | Omit = omit, + search_user_id: SequenceNotStr[str] | Omit = omit, + service: str | 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, + ) -> AsyncBinaryAPIResponse: + """ + Download an organization's audit log records for a time range as a file, for + archival, compliance, or offline analysis. For interactive browsing, use GET + /audit-logs. + + Args: + end: Upper bound (exclusive) for the audit record timestamp. + + start: Lower bound (inclusive) for the audit record timestamp. + + auth_strategy: Filter by authentication strategy. + + cursor: Opaque cursor from X-Next-Cursor for the next chunk of older records. + + exclude_method: Filter out results by HTTP method. + + format: Encoding for the returned chunk. + + limit: Maximum number of records to return in this chunk. + + method: Filter by HTTP method. + + search: Free-text search over path, user ID, email, client IP, and status. + + search_user_id: Additional user IDs to OR into free-text search. + + service: Filter by service name. + + 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 + """ + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + return await self._get( + "/audit-logs/export/chunk", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "end": end, + "start": start, + "auth_strategy": auth_strategy, + "cursor": cursor, + "exclude_method": exclude_method, + "format": format, + "limit": limit, + "method": method, + "search": search, + "search_user_id": search_user_id, + "service": service, + }, + audit_log_export_chunk_params.AuditLogExportChunkParams, + ), + ), + cast_to=AsyncBinaryAPIResponse, + ) + class AuditLogsResourceWithRawResponse: def __init__(self, audit_logs: AuditLogsResource) -> None: @@ -240,6 +419,10 @@ def __init__(self, audit_logs: AuditLogsResource) -> None: self.list = to_raw_response_wrapper( audit_logs.list, ) + self.export_chunk = to_custom_raw_response_wrapper( + audit_logs.export_chunk, + BinaryAPIResponse, + ) class AsyncAuditLogsResourceWithRawResponse: @@ -249,6 +432,10 @@ def __init__(self, audit_logs: AsyncAuditLogsResource) -> None: self.list = async_to_raw_response_wrapper( audit_logs.list, ) + self.export_chunk = async_to_custom_raw_response_wrapper( + audit_logs.export_chunk, + AsyncBinaryAPIResponse, + ) class AuditLogsResourceWithStreamingResponse: @@ -258,6 +445,10 @@ def __init__(self, audit_logs: AuditLogsResource) -> None: self.list = to_streamed_response_wrapper( audit_logs.list, ) + self.export_chunk = to_custom_streamed_response_wrapper( + audit_logs.export_chunk, + StreamedBinaryAPIResponse, + ) class AsyncAuditLogsResourceWithStreamingResponse: @@ -267,3 +458,7 @@ def __init__(self, audit_logs: AsyncAuditLogsResource) -> None: self.list = async_to_streamed_response_wrapper( audit_logs.list, ) + self.export_chunk = async_to_custom_streamed_response_wrapper( + audit_logs.export_chunk, + AsyncStreamedBinaryAPIResponse, + ) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 357afc9..49af1d5 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -90,6 +90,7 @@ from .browser_pool_release_params import BrowserPoolReleaseParams as BrowserPoolReleaseParams from .deployment_retrieve_response import DeploymentRetrieveResponse as DeploymentRetrieveResponse from .invocation_retrieve_response import InvocationRetrieveResponse as InvocationRetrieveResponse +from .audit_log_export_chunk_params import AuditLogExportChunkParams as AuditLogExportChunkParams from .browser_pool_acquire_response import BrowserPoolAcquireResponse as BrowserPoolAcquireResponse from .credential_totp_code_response import CredentialTotpCodeResponse as CredentialTotpCodeResponse from .browser_load_extensions_params import BrowserLoadExtensionsParams as BrowserLoadExtensionsParams diff --git a/src/kernel/types/audit_log_export_chunk_params.py b/src/kernel/types/audit_log_export_chunk_params.py new file mode 100644 index 0000000..8e759c9 --- /dev/null +++ b/src/kernel/types/audit_log_export_chunk_params.py @@ -0,0 +1,47 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union +from datetime import datetime +from typing_extensions import Literal, Required, Annotated, TypedDict + +from .._types import SequenceNotStr +from .._utils import PropertyInfo + +__all__ = ["AuditLogExportChunkParams"] + + +class AuditLogExportChunkParams(TypedDict, total=False): + end: Required[Annotated[Union[str, datetime], PropertyInfo(format="iso8601")]] + """Upper bound (exclusive) for the audit record timestamp.""" + + start: Required[Annotated[Union[str, datetime], PropertyInfo(format="iso8601")]] + """Lower bound (inclusive) for the audit record timestamp.""" + + auth_strategy: str + """Filter by authentication strategy.""" + + cursor: str + """Opaque cursor from X-Next-Cursor for the next chunk of older records.""" + + exclude_method: str + """Filter out results by HTTP method.""" + + format: Literal["jsonl", "jsonl.gz"] + """Encoding for the returned chunk.""" + + limit: int + """Maximum number of records to return in this chunk.""" + + method: str + """Filter by HTTP method.""" + + search: str + """Free-text search over path, user ID, email, client IP, and status.""" + + search_user_id: SequenceNotStr[str] + """Additional user IDs to OR into free-text search.""" + + service: str + """Filter by service name.""" diff --git a/tests/api_resources/test_audit_logs.py b/tests/api_resources/test_audit_logs.py index 669a887..47193f0 100644 --- a/tests/api_resources/test_audit_logs.py +++ b/tests/api_resources/test_audit_logs.py @@ -5,12 +5,20 @@ import os from typing import Any, cast +import httpx import pytest +from respx import MockRouter from kernel import Kernel, AsyncKernel from tests.utils import assert_matches_type from kernel.types import AuditLogEntry from kernel._utils import parse_datetime +from kernel._response import ( + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, +) from kernel.pagination import SyncPageTokenPagination, AsyncPageTokenPagination base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -73,6 +81,73 @@ def test_streaming_response_list(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_method_export_chunk(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/audit-logs/export/chunk").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + audit_log = client.audit_logs.export_chunk( + end=parse_datetime("2026-01-02T00:00:00Z"), + start=parse_datetime("2026-01-01T00:00:00Z"), + ) + assert audit_log.is_closed + assert audit_log.json() == {"foo": "bar"} + assert cast(Any, audit_log.is_closed) is True + assert isinstance(audit_log, BinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_method_export_chunk_with_all_params(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/audit-logs/export/chunk").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + audit_log = client.audit_logs.export_chunk( + end=parse_datetime("2026-01-02T00:00:00Z"), + start=parse_datetime("2026-01-01T00:00:00Z"), + auth_strategy="auth_strategy", + cursor="cursor", + exclude_method="exclude_method", + format="jsonl", + limit=1, + method="method", + search="search", + search_user_id=["string"], + service="service", + ) + assert audit_log.is_closed + assert audit_log.json() == {"foo": "bar"} + assert cast(Any, audit_log.is_closed) is True + assert isinstance(audit_log, BinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_raw_response_export_chunk(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/audit-logs/export/chunk").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + audit_log = client.audit_logs.with_raw_response.export_chunk( + end=parse_datetime("2026-01-02T00:00:00Z"), + start=parse_datetime("2026-01-01T00:00:00Z"), + ) + + assert audit_log.is_closed is True + assert audit_log.http_request.headers.get("X-Stainless-Lang") == "python" + assert audit_log.json() == {"foo": "bar"} + assert isinstance(audit_log, BinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_streaming_response_export_chunk(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/audit-logs/export/chunk").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + with client.audit_logs.with_streaming_response.export_chunk( + end=parse_datetime("2026-01-02T00:00:00Z"), + start=parse_datetime("2026-01-01T00:00:00Z"), + ) as audit_log: + assert not audit_log.is_closed + assert audit_log.http_request.headers.get("X-Stainless-Lang") == "python" + + assert audit_log.json() == {"foo": "bar"} + assert cast(Any, audit_log.is_closed) is True + assert isinstance(audit_log, StreamedBinaryAPIResponse) + + assert cast(Any, audit_log.is_closed) is True + class TestAsyncAuditLogs: parametrize = pytest.mark.parametrize( @@ -132,3 +207,70 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: assert_matches_type(AsyncPageTokenPagination[AuditLogEntry], audit_log, path=["response"]) assert cast(Any, response.is_closed) is True + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_method_export_chunk(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/audit-logs/export/chunk").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + audit_log = await async_client.audit_logs.export_chunk( + end=parse_datetime("2026-01-02T00:00:00Z"), + start=parse_datetime("2026-01-01T00:00:00Z"), + ) + assert audit_log.is_closed + assert await audit_log.json() == {"foo": "bar"} + assert cast(Any, audit_log.is_closed) is True + assert isinstance(audit_log, AsyncBinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_method_export_chunk_with_all_params(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/audit-logs/export/chunk").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + audit_log = await async_client.audit_logs.export_chunk( + end=parse_datetime("2026-01-02T00:00:00Z"), + start=parse_datetime("2026-01-01T00:00:00Z"), + auth_strategy="auth_strategy", + cursor="cursor", + exclude_method="exclude_method", + format="jsonl", + limit=1, + method="method", + search="search", + search_user_id=["string"], + service="service", + ) + assert audit_log.is_closed + assert await audit_log.json() == {"foo": "bar"} + assert cast(Any, audit_log.is_closed) is True + assert isinstance(audit_log, AsyncBinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_raw_response_export_chunk(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/audit-logs/export/chunk").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + audit_log = await async_client.audit_logs.with_raw_response.export_chunk( + end=parse_datetime("2026-01-02T00:00:00Z"), + start=parse_datetime("2026-01-01T00:00:00Z"), + ) + + assert audit_log.is_closed is True + assert audit_log.http_request.headers.get("X-Stainless-Lang") == "python" + assert await audit_log.json() == {"foo": "bar"} + assert isinstance(audit_log, AsyncBinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_streaming_response_export_chunk(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/audit-logs/export/chunk").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + async with async_client.audit_logs.with_streaming_response.export_chunk( + end=parse_datetime("2026-01-02T00:00:00Z"), + start=parse_datetime("2026-01-01T00:00:00Z"), + ) as audit_log: + assert not audit_log.is_closed + assert audit_log.http_request.headers.get("X-Stainless-Lang") == "python" + + assert await audit_log.json() == {"foo": "bar"} + assert cast(Any, audit_log.is_closed) is True + assert isinstance(audit_log, AsyncStreamedBinaryAPIResponse) + + assert cast(Any, audit_log.is_closed) is True