From 4818d2d6084684529935c8e6b9b109516a1de373 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 5 Sep 2025 03:38:14 +0000
Subject: [PATCH 1/6] chore(internal): move mypy configurations to
`pyproject.toml` file
---
mypy.ini | 50 ------------------------------------------------
pyproject.toml | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 52 insertions(+), 50 deletions(-)
delete mode 100644 mypy.ini
diff --git a/mypy.ini b/mypy.ini
deleted file mode 100644
index 0745431c..00000000
--- a/mypy.ini
+++ /dev/null
@@ -1,50 +0,0 @@
-[mypy]
-pretty = True
-show_error_codes = True
-
-# Exclude _files.py because mypy isn't smart enough to apply
-# the correct type narrowing and as this is an internal module
-# it's fine to just use Pyright.
-#
-# We also exclude our `tests` as mypy doesn't always infer
-# types correctly and Pyright will still catch any type errors.
-exclude = ^(src/kernel/_files\.py|_dev/.*\.py|tests/.*)$
-
-strict_equality = True
-implicit_reexport = True
-check_untyped_defs = True
-no_implicit_optional = True
-
-warn_return_any = True
-warn_unreachable = True
-warn_unused_configs = True
-
-# Turn these options off as it could cause conflicts
-# with the Pyright options.
-warn_unused_ignores = False
-warn_redundant_casts = False
-
-disallow_any_generics = True
-disallow_untyped_defs = True
-disallow_untyped_calls = True
-disallow_subclassing_any = True
-disallow_incomplete_defs = True
-disallow_untyped_decorators = True
-cache_fine_grained = True
-
-# By default, mypy reports an error if you assign a value to the result
-# of a function call that doesn't return anything. We do this in our test
-# cases:
-# ```
-# result = ...
-# assert result is None
-# ```
-# Changing this codegen to make mypy happy would increase complexity
-# and would not be worth it.
-disable_error_code = func-returns-value,overload-cannot-match
-
-# https://github.com/python/mypy/issues/12162
-[mypy.overrides]
-module = "black.files.*"
-ignore_errors = true
-ignore_missing_imports = true
diff --git a/pyproject.toml b/pyproject.toml
index f731ab32..2acfd24c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -157,6 +157,58 @@ reportOverlappingOverload = false
reportImportCycles = false
reportPrivateUsage = false
+[tool.mypy]
+pretty = true
+show_error_codes = true
+
+# Exclude _files.py because mypy isn't smart enough to apply
+# the correct type narrowing and as this is an internal module
+# it's fine to just use Pyright.
+#
+# We also exclude our `tests` as mypy doesn't always infer
+# types correctly and Pyright will still catch any type errors.
+exclude = ['src/kernel/_files.py', '_dev/.*.py', 'tests/.*']
+
+strict_equality = true
+implicit_reexport = true
+check_untyped_defs = true
+no_implicit_optional = true
+
+warn_return_any = true
+warn_unreachable = true
+warn_unused_configs = true
+
+# Turn these options off as it could cause conflicts
+# with the Pyright options.
+warn_unused_ignores = false
+warn_redundant_casts = false
+
+disallow_any_generics = true
+disallow_untyped_defs = true
+disallow_untyped_calls = true
+disallow_subclassing_any = true
+disallow_incomplete_defs = true
+disallow_untyped_decorators = true
+cache_fine_grained = true
+
+# By default, mypy reports an error if you assign a value to the result
+# of a function call that doesn't return anything. We do this in our test
+# cases:
+# ```
+# result = ...
+# assert result is None
+# ```
+# Changing this codegen to make mypy happy would increase complexity
+# and would not be worth it.
+disable_error_code = "func-returns-value,overload-cannot-match"
+
+# https://github.com/python/mypy/issues/12162
+[[tool.mypy.overrides]]
+module = "black.files.*"
+ignore_errors = true
+ignore_missing_imports = true
+
+
[tool.ruff]
line-length = 120
output-format = "grouped"
From e5838f51b9af325700b23d55ff2bb11b6ff3306e Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 5 Sep 2025 15:16:51 +0000
Subject: [PATCH 2/6] feat(api): add pagination to the deployments endpoint
---
.stats.yml | 6 +-
README.md | 73 ++++++++++++++++++
api.md | 2 +-
src/kernel/pagination.py | 78 ++++++++++++++++++++
src/kernel/resources/deployments.py | 53 ++++++++++---
src/kernel/types/deployment_list_params.py | 10 ++-
src/kernel/types/deployment_list_response.py | 11 +--
tests/api_resources/test_deployments.py | 45 +++++++----
8 files changed, 239 insertions(+), 39 deletions(-)
create mode 100644 src/kernel/pagination.py
diff --git a/.stats.yml b/.stats.yml
index 6ac19baf..8943c2f3 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 46
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-e98d46c55826cdf541a9ee0df04ce92806ac6d4d92957ae79f897270b7d85b23.yml
-openapi_spec_hash: 8a1af54fc0a4417165b8a52e6354b685
-config_hash: 043ddc54629c6d8b889123770cb4769f
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-33f1feaba7bde46bfa36d2fefb5c3bc9512967945bccf78045ad3f64aafc4eb0.yml
+openapi_spec_hash: 52a448889d41216d1ca30e8a57115b14
+config_hash: 1f28d5c3c063f418ebd2799df1e4e781
diff --git a/README.md b/README.md
index 884d10c2..c5b5bf7c 100644
--- a/README.md
+++ b/README.md
@@ -115,6 +115,79 @@ Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typ
Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`.
+## Pagination
+
+List methods in the Kernel API are paginated.
+
+This library provides auto-paginating iterators with each list response, so you do not have to request successive pages manually:
+
+```python
+from kernel import Kernel
+
+client = Kernel()
+
+all_deployments = []
+# Automatically fetches more pages as needed.
+for deployment in client.deployments.list(
+ app_name="YOUR_APP",
+ limit=2,
+):
+ # Do something with deployment here
+ all_deployments.append(deployment)
+print(all_deployments)
+```
+
+Or, asynchronously:
+
+```python
+import asyncio
+from kernel import AsyncKernel
+
+client = AsyncKernel()
+
+
+async def main() -> None:
+ all_deployments = []
+ # Iterate through items across all pages, issuing requests as needed.
+ async for deployment in client.deployments.list(
+ app_name="YOUR_APP",
+ limit=2,
+ ):
+ all_deployments.append(deployment)
+ print(all_deployments)
+
+
+asyncio.run(main())
+```
+
+Alternatively, you can use the `.has_next_page()`, `.next_page_info()`, or `.get_next_page()` methods for more granular control working with pages:
+
+```python
+first_page = await client.deployments.list(
+ app_name="YOUR_APP",
+ limit=2,
+)
+if first_page.has_next_page():
+ print(f"will fetch next page using these details: {first_page.next_page_info()}")
+ next_page = await first_page.get_next_page()
+ print(f"number of items we just fetched: {len(next_page.items)}")
+
+# Remove `await` for non-async usage.
+```
+
+Or just work directly with the returned data:
+
+```python
+first_page = await client.deployments.list(
+ app_name="YOUR_APP",
+ limit=2,
+)
+for deployment in first_page.items:
+ print(deployment.id)
+
+# Remove `await` for non-async usage.
+```
+
## Nested params
Nested parameters are dictionaries, typed using `TypedDict`, for example:
diff --git a/api.md b/api.md
index 78295850..56d852bc 100644
--- a/api.md
+++ b/api.md
@@ -22,7 +22,7 @@ Methods:
- client.deployments.create(\*\*params) -> DeploymentCreateResponse
- client.deployments.retrieve(id) -> DeploymentRetrieveResponse
-- client.deployments.list(\*\*params) -> DeploymentListResponse
+- client.deployments.list(\*\*params) -> SyncOffsetPagination[DeploymentListResponse]
- client.deployments.follow(id, \*\*params) -> DeploymentFollowResponse
# Apps
diff --git a/src/kernel/pagination.py b/src/kernel/pagination.py
new file mode 100644
index 00000000..2002d5eb
--- /dev/null
+++ b/src/kernel/pagination.py
@@ -0,0 +1,78 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Any, List, Type, Generic, Mapping, TypeVar, Optional, cast
+from typing_extensions import override
+
+from httpx import Response
+
+from ._utils import is_mapping
+from ._models import BaseModel
+from ._base_client import BasePage, PageInfo, BaseSyncPage, BaseAsyncPage
+
+__all__ = ["SyncOffsetPagination", "AsyncOffsetPagination"]
+
+_BaseModelT = TypeVar("_BaseModelT", bound=BaseModel)
+
+_T = TypeVar("_T")
+
+
+class SyncOffsetPagination(BaseSyncPage[_T], BasePage[_T], Generic[_T]):
+ items: List[_T]
+
+ @override
+ def _get_page_items(self) -> List[_T]:
+ items = self.items
+ if not items:
+ return []
+ return items
+
+ @override
+ def next_page_info(self) -> Optional[PageInfo]:
+ offset = self._options.params.get("offset") or 0
+ if not isinstance(offset, int):
+ raise ValueError(f'Expected "offset" param to be an integer but got {offset}')
+
+ length = len(self._get_page_items())
+ current_count = offset + length
+
+ return PageInfo(params={"offset": current_count})
+
+ @classmethod
+ def build(cls: Type[_BaseModelT], *, response: Response, data: object) -> _BaseModelT: # noqa: ARG003
+ return cls.construct(
+ None,
+ **{
+ **(cast(Mapping[str, Any], data) if is_mapping(data) else {"items": data}),
+ },
+ )
+
+
+class AsyncOffsetPagination(BaseAsyncPage[_T], BasePage[_T], Generic[_T]):
+ items: List[_T]
+
+ @override
+ def _get_page_items(self) -> List[_T]:
+ items = self.items
+ if not items:
+ return []
+ return items
+
+ @override
+ def next_page_info(self) -> Optional[PageInfo]:
+ offset = self._options.params.get("offset") or 0
+ if not isinstance(offset, int):
+ raise ValueError(f'Expected "offset" param to be an integer but got {offset}')
+
+ length = len(self._get_page_items())
+ current_count = offset + length
+
+ return PageInfo(params={"offset": current_count})
+
+ @classmethod
+ def build(cls: Type[_BaseModelT], *, response: Response, data: object) -> _BaseModelT: # noqa: ARG003
+ return cls.construct(
+ None,
+ **{
+ **(cast(Mapping[str, Any], data) if is_mapping(data) else {"items": data}),
+ },
+ )
diff --git a/src/kernel/resources/deployments.py b/src/kernel/resources/deployments.py
index d54c4ec2..a288798e 100644
--- a/src/kernel/resources/deployments.py
+++ b/src/kernel/resources/deployments.py
@@ -19,7 +19,8 @@
async_to_streamed_response_wrapper,
)
from .._streaming import Stream, AsyncStream
-from .._base_client import make_request_options
+from ..pagination import SyncOffsetPagination, AsyncOffsetPagination
+from .._base_client import AsyncPaginator, make_request_options
from ..types.deployment_list_response import DeploymentListResponse
from ..types.deployment_create_response import DeploymentCreateResponse
from ..types.deployment_follow_response import DeploymentFollowResponse
@@ -150,14 +151,16 @@ def retrieve(
def list(
self,
*,
- app_name: str | NotGiven = NOT_GIVEN,
+ app_name: str,
+ limit: int | NotGiven = NOT_GIVEN,
+ offset: int | NotGiven = NOT_GIVEN,
# 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,
- ) -> DeploymentListResponse:
+ ) -> SyncOffsetPagination[DeploymentListResponse]:
"""List deployments.
Optionally filter by application name.
@@ -165,6 +168,10 @@ def list(
Args:
app_name: Filter results by application name.
+ limit: Limit the number of deployments to return.
+
+ offset: Offset the number of deployments to return.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -173,16 +180,24 @@ def list(
timeout: Override the client-level default timeout for this request, in seconds
"""
- return self._get(
+ return self._get_api_list(
"/deployments",
+ page=SyncOffsetPagination[DeploymentListResponse],
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
extra_body=extra_body,
timeout=timeout,
- query=maybe_transform({"app_name": app_name}, deployment_list_params.DeploymentListParams),
+ query=maybe_transform(
+ {
+ "app_name": app_name,
+ "limit": limit,
+ "offset": offset,
+ },
+ deployment_list_params.DeploymentListParams,
+ ),
),
- cast_to=DeploymentListResponse,
+ model=DeploymentListResponse,
)
def follow(
@@ -352,17 +367,19 @@ async def retrieve(
cast_to=DeploymentRetrieveResponse,
)
- async def list(
+ def list(
self,
*,
- app_name: str | NotGiven = NOT_GIVEN,
+ app_name: str,
+ limit: int | NotGiven = NOT_GIVEN,
+ offset: int | NotGiven = NOT_GIVEN,
# 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,
- ) -> DeploymentListResponse:
+ ) -> AsyncPaginator[DeploymentListResponse, AsyncOffsetPagination[DeploymentListResponse]]:
"""List deployments.
Optionally filter by application name.
@@ -370,6 +387,10 @@ async def list(
Args:
app_name: Filter results by application name.
+ limit: Limit the number of deployments to return.
+
+ offset: Offset the number of deployments to return.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -378,16 +399,24 @@ async def list(
timeout: Override the client-level default timeout for this request, in seconds
"""
- return await self._get(
+ return self._get_api_list(
"/deployments",
+ page=AsyncOffsetPagination[DeploymentListResponse],
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
extra_body=extra_body,
timeout=timeout,
- query=await async_maybe_transform({"app_name": app_name}, deployment_list_params.DeploymentListParams),
+ query=maybe_transform(
+ {
+ "app_name": app_name,
+ "limit": limit,
+ "offset": offset,
+ },
+ deployment_list_params.DeploymentListParams,
+ ),
),
- cast_to=DeploymentListResponse,
+ model=DeploymentListResponse,
)
async def follow(
diff --git a/src/kernel/types/deployment_list_params.py b/src/kernel/types/deployment_list_params.py
index 05704a19..b39b446a 100644
--- a/src/kernel/types/deployment_list_params.py
+++ b/src/kernel/types/deployment_list_params.py
@@ -2,11 +2,17 @@
from __future__ import annotations
-from typing_extensions import TypedDict
+from typing_extensions import Required, TypedDict
__all__ = ["DeploymentListParams"]
class DeploymentListParams(TypedDict, total=False):
- app_name: str
+ app_name: Required[str]
"""Filter results by application name."""
+
+ limit: int
+ """Limit the number of deployments to return."""
+
+ offset: int
+ """Offset the number of deployments to return."""
diff --git a/src/kernel/types/deployment_list_response.py b/src/kernel/types/deployment_list_response.py
index ba7759da..d22b0076 100644
--- a/src/kernel/types/deployment_list_response.py
+++ b/src/kernel/types/deployment_list_response.py
@@ -1,15 +1,15 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import Dict, List, Optional
+from typing import Dict, Optional
from datetime import datetime
-from typing_extensions import Literal, TypeAlias
+from typing_extensions import Literal
from .._models import BaseModel
-__all__ = ["DeploymentListResponse", "DeploymentListResponseItem"]
+__all__ = ["DeploymentListResponse"]
-class DeploymentListResponseItem(BaseModel):
+class DeploymentListResponse(BaseModel):
id: str
"""Unique identifier for the deployment"""
@@ -33,6 +33,3 @@ class DeploymentListResponseItem(BaseModel):
updated_at: Optional[datetime] = None
"""Timestamp when the deployment was last updated"""
-
-
-DeploymentListResponse: TypeAlias = List[DeploymentListResponseItem]
diff --git a/tests/api_resources/test_deployments.py b/tests/api_resources/test_deployments.py
index c177978b..97a90a8a 100644
--- a/tests/api_resources/test_deployments.py
+++ b/tests/api_resources/test_deployments.py
@@ -14,6 +14,7 @@
DeploymentCreateResponse,
DeploymentRetrieveResponse,
)
+from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -116,36 +117,44 @@ def test_path_params_retrieve(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Prism tests are disabled")
@parametrize
def test_method_list(self, client: Kernel) -> None:
- deployment = client.deployments.list()
- assert_matches_type(DeploymentListResponse, deployment, path=["response"])
+ deployment = client.deployments.list(
+ app_name="app_name",
+ )
+ assert_matches_type(SyncOffsetPagination[DeploymentListResponse], deployment, path=["response"])
@pytest.mark.skip(reason="Prism tests are disabled")
@parametrize
def test_method_list_with_all_params(self, client: Kernel) -> None:
deployment = client.deployments.list(
app_name="app_name",
+ limit=1,
+ offset=0,
)
- assert_matches_type(DeploymentListResponse, deployment, path=["response"])
+ assert_matches_type(SyncOffsetPagination[DeploymentListResponse], deployment, path=["response"])
@pytest.mark.skip(reason="Prism tests are disabled")
@parametrize
def test_raw_response_list(self, client: Kernel) -> None:
- response = client.deployments.with_raw_response.list()
+ response = client.deployments.with_raw_response.list(
+ app_name="app_name",
+ )
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
deployment = response.parse()
- assert_matches_type(DeploymentListResponse, deployment, path=["response"])
+ assert_matches_type(SyncOffsetPagination[DeploymentListResponse], deployment, path=["response"])
@pytest.mark.skip(reason="Prism tests are disabled")
@parametrize
def test_streaming_response_list(self, client: Kernel) -> None:
- with client.deployments.with_streaming_response.list() as response:
+ with client.deployments.with_streaming_response.list(
+ app_name="app_name",
+ ) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
deployment = response.parse()
- assert_matches_type(DeploymentListResponse, deployment, path=["response"])
+ assert_matches_type(SyncOffsetPagination[DeploymentListResponse], deployment, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -300,36 +309,44 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None:
@pytest.mark.skip(reason="Prism tests are disabled")
@parametrize
async def test_method_list(self, async_client: AsyncKernel) -> None:
- deployment = await async_client.deployments.list()
- assert_matches_type(DeploymentListResponse, deployment, path=["response"])
+ deployment = await async_client.deployments.list(
+ app_name="app_name",
+ )
+ assert_matches_type(AsyncOffsetPagination[DeploymentListResponse], deployment, path=["response"])
@pytest.mark.skip(reason="Prism tests are disabled")
@parametrize
async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None:
deployment = await async_client.deployments.list(
app_name="app_name",
+ limit=1,
+ offset=0,
)
- assert_matches_type(DeploymentListResponse, deployment, path=["response"])
+ assert_matches_type(AsyncOffsetPagination[DeploymentListResponse], deployment, path=["response"])
@pytest.mark.skip(reason="Prism tests are disabled")
@parametrize
async def test_raw_response_list(self, async_client: AsyncKernel) -> None:
- response = await async_client.deployments.with_raw_response.list()
+ response = await async_client.deployments.with_raw_response.list(
+ app_name="app_name",
+ )
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
deployment = await response.parse()
- assert_matches_type(DeploymentListResponse, deployment, path=["response"])
+ assert_matches_type(AsyncOffsetPagination[DeploymentListResponse], deployment, path=["response"])
@pytest.mark.skip(reason="Prism tests are disabled")
@parametrize
async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
- async with async_client.deployments.with_streaming_response.list() as response:
+ async with async_client.deployments.with_streaming_response.list(
+ app_name="app_name",
+ ) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
deployment = await response.parse()
- assert_matches_type(DeploymentListResponse, deployment, path=["response"])
+ assert_matches_type(AsyncOffsetPagination[DeploymentListResponse], deployment, path=["response"])
assert cast(Any, response.is_closed) is True
From f64f55b00b0e0fa19dd2162cd914001381254314 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 5 Sep 2025 15:19:41 +0000
Subject: [PATCH 3/6] feat(api): update API spec with pagination headers
---
.stats.yml | 4 ++--
src/kernel/resources/deployments.py | 4 ++--
src/kernel/types/deployment_list_params.py | 4 ++--
tests/api_resources/test_deployments.py | 24 ++++++----------------
4 files changed, 12 insertions(+), 24 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 8943c2f3..9635bb22 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 46
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-33f1feaba7bde46bfa36d2fefb5c3bc9512967945bccf78045ad3f64aafc4eb0.yml
-openapi_spec_hash: 52a448889d41216d1ca30e8a57115b14
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-cb38560915edce03abce2ae3ef5bc745489dbe9b6f80c2b4ff42edf8c2ff276d.yml
+openapi_spec_hash: a869194d6c864ba28d79ec0105439c3e
config_hash: 1f28d5c3c063f418ebd2799df1e4e781
diff --git a/src/kernel/resources/deployments.py b/src/kernel/resources/deployments.py
index a288798e..5c7715de 100644
--- a/src/kernel/resources/deployments.py
+++ b/src/kernel/resources/deployments.py
@@ -151,7 +151,7 @@ def retrieve(
def list(
self,
*,
- app_name: str,
+ app_name: str | NotGiven = NOT_GIVEN,
limit: int | NotGiven = NOT_GIVEN,
offset: int | NotGiven = NOT_GIVEN,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -370,7 +370,7 @@ async def retrieve(
def list(
self,
*,
- app_name: str,
+ app_name: str | NotGiven = NOT_GIVEN,
limit: int | NotGiven = NOT_GIVEN,
offset: int | NotGiven = NOT_GIVEN,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
diff --git a/src/kernel/types/deployment_list_params.py b/src/kernel/types/deployment_list_params.py
index b39b446a..54124da5 100644
--- a/src/kernel/types/deployment_list_params.py
+++ b/src/kernel/types/deployment_list_params.py
@@ -2,13 +2,13 @@
from __future__ import annotations
-from typing_extensions import Required, TypedDict
+from typing_extensions import TypedDict
__all__ = ["DeploymentListParams"]
class DeploymentListParams(TypedDict, total=False):
- app_name: Required[str]
+ app_name: str
"""Filter results by application name."""
limit: int
diff --git a/tests/api_resources/test_deployments.py b/tests/api_resources/test_deployments.py
index 97a90a8a..fc5d2991 100644
--- a/tests/api_resources/test_deployments.py
+++ b/tests/api_resources/test_deployments.py
@@ -117,9 +117,7 @@ def test_path_params_retrieve(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Prism tests are disabled")
@parametrize
def test_method_list(self, client: Kernel) -> None:
- deployment = client.deployments.list(
- app_name="app_name",
- )
+ deployment = client.deployments.list()
assert_matches_type(SyncOffsetPagination[DeploymentListResponse], deployment, path=["response"])
@pytest.mark.skip(reason="Prism tests are disabled")
@@ -135,9 +133,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Prism tests are disabled")
@parametrize
def test_raw_response_list(self, client: Kernel) -> None:
- response = client.deployments.with_raw_response.list(
- app_name="app_name",
- )
+ response = client.deployments.with_raw_response.list()
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -147,9 +143,7 @@ def test_raw_response_list(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Prism tests are disabled")
@parametrize
def test_streaming_response_list(self, client: Kernel) -> None:
- with client.deployments.with_streaming_response.list(
- app_name="app_name",
- ) as response:
+ with client.deployments.with_streaming_response.list() as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -309,9 +303,7 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None:
@pytest.mark.skip(reason="Prism tests are disabled")
@parametrize
async def test_method_list(self, async_client: AsyncKernel) -> None:
- deployment = await async_client.deployments.list(
- app_name="app_name",
- )
+ deployment = await async_client.deployments.list()
assert_matches_type(AsyncOffsetPagination[DeploymentListResponse], deployment, path=["response"])
@pytest.mark.skip(reason="Prism tests are disabled")
@@ -327,9 +319,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N
@pytest.mark.skip(reason="Prism tests are disabled")
@parametrize
async def test_raw_response_list(self, async_client: AsyncKernel) -> None:
- response = await async_client.deployments.with_raw_response.list(
- app_name="app_name",
- )
+ response = await async_client.deployments.with_raw_response.list()
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -339,9 +329,7 @@ async def test_raw_response_list(self, async_client: AsyncKernel) -> None:
@pytest.mark.skip(reason="Prism tests are disabled")
@parametrize
async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
- async with async_client.deployments.with_streaming_response.list(
- app_name="app_name",
- ) as response:
+ async with async_client.deployments.with_streaming_response.list() as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
From 5f2329f8712b9d1865cc95dcde06834fe65622ee Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 5 Sep 2025 18:00:37 +0000
Subject: [PATCH 4/6] feat(api): pagination properties added to response
(has_more, next_offset)
---
.stats.yml | 2 +-
README.md | 4 ++++
src/kernel/pagination.py | 42 +++++++++++++++++++++++++++++++---------
3 files changed, 38 insertions(+), 10 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 9635bb22..7fb3d31e 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 46
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-cb38560915edce03abce2ae3ef5bc745489dbe9b6f80c2b4ff42edf8c2ff276d.yml
openapi_spec_hash: a869194d6c864ba28d79ec0105439c3e
-config_hash: 1f28d5c3c063f418ebd2799df1e4e781
+config_hash: ed56f95781ec9b2e73c97e1a66606071
diff --git a/README.md b/README.md
index c5b5bf7c..beec3f01 100644
--- a/README.md
+++ b/README.md
@@ -182,6 +182,10 @@ first_page = await client.deployments.list(
app_name="YOUR_APP",
limit=2,
)
+
+print(
+ f"the current start offset for this page: {first_page.next_offset}"
+) # => "the current start offset for this page: 1"
for deployment in first_page.items:
print(deployment.id)
diff --git a/src/kernel/pagination.py b/src/kernel/pagination.py
index 2002d5eb..cdf83c2f 100644
--- a/src/kernel/pagination.py
+++ b/src/kernel/pagination.py
@@ -5,7 +5,7 @@
from httpx import Response
-from ._utils import is_mapping
+from ._utils import is_mapping, maybe_coerce_boolean, maybe_coerce_integer
from ._models import BaseModel
from ._base_client import BasePage, PageInfo, BaseSyncPage, BaseAsyncPage
@@ -18,6 +18,8 @@
class SyncOffsetPagination(BaseSyncPage[_T], BasePage[_T], Generic[_T]):
items: List[_T]
+ has_more: Optional[bool] = None
+ next_offset: Optional[int] = None
@override
def _get_page_items(self) -> List[_T]:
@@ -26,14 +28,22 @@ def _get_page_items(self) -> List[_T]:
return []
return items
+ @override
+ def has_next_page(self) -> bool:
+ has_more = self.has_more
+ if has_more is not None and has_more is False:
+ return False
+
+ return super().has_next_page()
+
@override
def next_page_info(self) -> Optional[PageInfo]:
- offset = self._options.params.get("offset") or 0
- if not isinstance(offset, int):
- raise ValueError(f'Expected "offset" param to be an integer but got {offset}')
+ next_offset = self.next_offset
+ if next_offset is None:
+ return None # type: ignore[unreachable]
length = len(self._get_page_items())
- current_count = offset + length
+ current_count = next_offset + length
return PageInfo(params={"offset": current_count})
@@ -43,12 +53,16 @@ def build(cls: Type[_BaseModelT], *, response: Response, data: object) -> _BaseM
None,
**{
**(cast(Mapping[str, Any], data) if is_mapping(data) else {"items": data}),
+ "has_more": maybe_coerce_boolean(response.headers.get("X-Has-More")),
+ "next_offset": maybe_coerce_integer(response.headers.get("X-Next-Offset")),
},
)
class AsyncOffsetPagination(BaseAsyncPage[_T], BasePage[_T], Generic[_T]):
items: List[_T]
+ has_more: Optional[bool] = None
+ next_offset: Optional[int] = None
@override
def _get_page_items(self) -> List[_T]:
@@ -57,14 +71,22 @@ def _get_page_items(self) -> List[_T]:
return []
return items
+ @override
+ def has_next_page(self) -> bool:
+ has_more = self.has_more
+ if has_more is not None and has_more is False:
+ return False
+
+ return super().has_next_page()
+
@override
def next_page_info(self) -> Optional[PageInfo]:
- offset = self._options.params.get("offset") or 0
- if not isinstance(offset, int):
- raise ValueError(f'Expected "offset" param to be an integer but got {offset}')
+ next_offset = self.next_offset
+ if next_offset is None:
+ return None # type: ignore[unreachable]
length = len(self._get_page_items())
- current_count = offset + length
+ current_count = next_offset + length
return PageInfo(params={"offset": current_count})
@@ -74,5 +96,7 @@ def build(cls: Type[_BaseModelT], *, response: Response, data: object) -> _BaseM
None,
**{
**(cast(Mapping[str, Any], data) if is_mapping(data) else {"items": data}),
+ "has_more": maybe_coerce_boolean(response.headers.get("X-Has-More")),
+ "next_offset": maybe_coerce_integer(response.headers.get("X-Next-Offset")),
},
)
From cd90a498d24b1f4490583bec64e5e670eb725197 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 6 Sep 2025 03:56:02 +0000
Subject: [PATCH 5/6] chore(tests): simplify `get_platform` test
`nest_asyncio` is archived and broken on some platforms so it's
not worth keeping in our test suite.
---
pyproject.toml | 1 -
requirements-dev.lock | 1 -
tests/test_client.py | 53 +++++--------------------------------------
3 files changed, 6 insertions(+), 49 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 2acfd24c..0486b41f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -56,7 +56,6 @@ dev-dependencies = [
"dirty-equals>=0.6.0",
"importlib-metadata>=6.7.0",
"rich>=13.7.1",
- "nest_asyncio==1.6.0",
"pytest-xdist>=3.6.1",
]
diff --git a/requirements-dev.lock b/requirements-dev.lock
index 55681a90..56d0accb 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -75,7 +75,6 @@ multidict==6.4.4
mypy==1.14.1
mypy-extensions==1.0.0
# via mypy
-nest-asyncio==1.6.0
nodeenv==1.8.0
# via pyright
nox==2023.4.22
diff --git a/tests/test_client.py b/tests/test_client.py
index d5a17848..74ffb05e 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -6,13 +6,10 @@
import os
import sys
import json
-import time
import asyncio
import inspect
-import subprocess
import tracemalloc
from typing import Any, Union, cast
-from textwrap import dedent
from unittest import mock
from typing_extensions import Literal
@@ -23,14 +20,17 @@
from kernel import Kernel, AsyncKernel, APIResponseValidationError
from kernel._types import Omit
+from kernel._utils import asyncify
from kernel._models import BaseModel, FinalRequestOptions
from kernel._exceptions import KernelError, APIStatusError, APITimeoutError, APIResponseValidationError
from kernel._base_client import (
DEFAULT_TIMEOUT,
HTTPX_DEFAULT_TIMEOUT,
BaseClient,
+ OtherPlatform,
DefaultHttpxClient,
DefaultAsyncHttpxClient,
+ get_platform,
make_request_options,
)
@@ -1645,50 +1645,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
assert response.http_request.headers.get("x-stainless-retry-count") == "42"
- def test_get_platform(self) -> None:
- # A previous implementation of asyncify could leave threads unterminated when
- # used with nest_asyncio.
- #
- # Since nest_asyncio.apply() is global and cannot be un-applied, this
- # test is run in a separate process to avoid affecting other tests.
- test_code = dedent("""
- import asyncio
- import nest_asyncio
- import threading
-
- from kernel._utils import asyncify
- from kernel._base_client import get_platform
-
- async def test_main() -> None:
- result = await asyncify(get_platform)()
- print(result)
- for thread in threading.enumerate():
- print(thread.name)
-
- nest_asyncio.apply()
- asyncio.run(test_main())
- """)
- with subprocess.Popen(
- [sys.executable, "-c", test_code],
- text=True,
- ) as process:
- timeout = 10 # seconds
-
- start_time = time.monotonic()
- while True:
- return_code = process.poll()
- if return_code is not None:
- if return_code != 0:
- raise AssertionError("calling get_platform using asyncify resulted in a non-zero exit code")
-
- # success
- break
-
- if time.monotonic() - start_time > timeout:
- process.kill()
- raise AssertionError("calling get_platform using asyncify resulted in a hung process")
-
- time.sleep(0.1)
+ async def test_get_platform(self) -> None:
+ platform = await asyncify(get_platform)()
+ assert isinstance(platform, (str, OtherPlatform))
async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None:
# Test that the proxy environment variables are set correctly
From 0287597eff74b9ba512305a74f00b328d3f61c46 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 6 Sep 2025 03:56:18 +0000
Subject: [PATCH 6/6] release: 0.11.1
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 16 ++++++++++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 19 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index f7014c35..e82003f4 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.11.0"
+ ".": "0.11.1"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a6a6cec2..5d1ad48f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,21 @@
# Changelog
+## 0.11.1 (2025-09-06)
+
+Full Changelog: [v0.11.0...v0.11.1](https://github.com/onkernel/kernel-python-sdk/compare/v0.11.0...v0.11.1)
+
+### Features
+
+* **api:** add pagination to the deployments endpoint ([e5838f5](https://github.com/onkernel/kernel-python-sdk/commit/e5838f51b9af325700b23d55ff2bb11b6ff3306e))
+* **api:** pagination properties added to response (has_more, next_offset) ([5f2329f](https://github.com/onkernel/kernel-python-sdk/commit/5f2329f8712b9d1865cc95dcde06834fe65622ee))
+* **api:** update API spec with pagination headers ([f64f55b](https://github.com/onkernel/kernel-python-sdk/commit/f64f55b00b0e0fa19dd2162cd914001381254314))
+
+
+### Chores
+
+* **internal:** move mypy configurations to `pyproject.toml` file ([4818d2d](https://github.com/onkernel/kernel-python-sdk/commit/4818d2d6084684529935c8e6b9b109516a1de373))
+* **tests:** simplify `get_platform` test ([cd90a49](https://github.com/onkernel/kernel-python-sdk/commit/cd90a498d24b1f4490583bec64e5e670eb725197))
+
## 0.11.0 (2025-09-04)
Full Changelog: [v0.10.0...v0.11.0](https://github.com/onkernel/kernel-python-sdk/compare/v0.10.0...v0.11.0)
diff --git a/pyproject.toml b/pyproject.toml
index 0486b41f..5533264c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.11.0"
+version = "0.11.1"
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 1caec76c..0e02001c 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.11.0" # x-release-please-version
+__version__ = "0.11.1" # x-release-please-version