diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index ac03171..e3778b2 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.6.1"
+ ".": "0.6.2"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index 4a84456..b296d07 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 16
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-b019e469425a59061f37c5fdc7a131a5291c66134ef0627db4f06bb1f4af0b15.yml
-openapi_spec_hash: f66a3c2efddb168db9539ba2507b10b8
-config_hash: aae6721b2be9ec8565dfc8f7eadfe105
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2aec229ccf91f7c1ac95aa675ea2a59bd61af9e363a22c3b49677992f1eeb16a.yml
+openapi_spec_hash: c80cd5d52a79cd5366a76d4a825bd27a
+config_hash: b8e1fff080fbaa22656ab0a57b591777
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0ad0718..b6b7e52 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,24 @@
# Changelog
+## 0.6.2 (2025-06-24)
+
+Full Changelog: [v0.6.1...v0.6.2](https://github.com/onkernel/kernel-python-sdk/compare/v0.6.1...v0.6.2)
+
+### Features
+
+* **api:** add `since` parameter to deployment logs endpoint ([39fb799](https://github.com/onkernel/kernel-python-sdk/commit/39fb79951c1f42c6eb7d07043432179ee132ff2c))
+* **client:** add support for aiohttp ([fbe32a1](https://github.com/onkernel/kernel-python-sdk/commit/fbe32a143a69f45cc8f93aab70d8fd555a337a9d))
+
+
+### Chores
+
+* **tests:** skip some failing tests on the latest python versions ([9441e05](https://github.com/onkernel/kernel-python-sdk/commit/9441e056d0a162b77149d717d83d75b67baf912b))
+
+
+### Documentation
+
+* **client:** fix httpx.Timeout documentation reference ([f3c0127](https://github.com/onkernel/kernel-python-sdk/commit/f3c0127bb4132bcf19ce2fd3016776c556386ffb))
+
## 0.6.1 (2025-06-18)
Full Changelog: [v0.6.0...v0.6.1](https://github.com/onkernel/kernel-python-sdk/compare/v0.6.0...v0.6.1)
diff --git a/README.md b/README.md
index fa501ab..00a4d1b 100644
--- a/README.md
+++ b/README.md
@@ -78,6 +78,43 @@ asyncio.run(main())
Functionality between the synchronous and asynchronous clients is otherwise identical.
+### With aiohttp
+
+By default, the async client uses `httpx` for HTTP requests. However, for improved concurrency performance you may also use `aiohttp` as the HTTP backend.
+
+You can enable this by installing `aiohttp`:
+
+```sh
+# install from PyPI
+pip install kernel[aiohttp]
+```
+
+Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`:
+
+```python
+import os
+import asyncio
+from kernel import DefaultAioHttpClient
+from kernel import AsyncKernel
+
+
+async def main() -> None:
+ async with AsyncKernel(
+ api_key=os.environ.get("KERNEL_API_KEY"), # This is the default and can be omitted
+ http_client=DefaultAioHttpClient(),
+ ) as client:
+ deployment = await client.apps.deployments.create(
+ entrypoint_rel_path="main.ts",
+ file=b"REPLACE_ME",
+ env_vars={"OPENAI_API_KEY": "x"},
+ version="1.0.0",
+ )
+ print(deployment.apps)
+
+
+asyncio.run(main())
+```
+
## Using types
Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev) which also provide helper methods for things like:
@@ -192,7 +229,7 @@ client.with_options(max_retries=5).browsers.create(
### Timeouts
By default requests time out after 1 minute. You can configure this with a `timeout` option,
-which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/#fine-tuning-the-configuration) object:
+which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/timeouts/#fine-tuning-the-configuration) object:
```python
from kernel import Kernel
diff --git a/api.md b/api.md
index 0127e61..c8f114f 100644
--- a/api.md
+++ b/api.md
@@ -1,7 +1,7 @@
# Shared Types
```python
-from kernel.types import ErrorDetail, ErrorEvent, ErrorModel, LogEvent
+from kernel.types import ErrorDetail, ErrorEvent, ErrorModel, HeartbeatEvent, LogEvent
```
# Deployments
@@ -21,7 +21,7 @@ Methods:
- client.deployments.create(\*\*params) -> DeploymentCreateResponse
- client.deployments.retrieve(id) -> DeploymentRetrieveResponse
-- client.deployments.follow(id) -> DeploymentFollowResponse
+- client.deployments.follow(id, \*\*params) -> DeploymentFollowResponse
# Apps
diff --git a/pyproject.toml b/pyproject.toml
index e783ee3..e5375bc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.6.1"
+version = "0.6.2"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
@@ -37,6 +37,8 @@ classifiers = [
Homepage = "https://github.com/onkernel/kernel-python-sdk"
Repository = "https://github.com/onkernel/kernel-python-sdk"
+[project.optional-dependencies]
+aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.6"]
[tool.rye]
managed = true
diff --git a/requirements-dev.lock b/requirements-dev.lock
index f40d985..27de013 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -10,6 +10,13 @@
# universal: false
-e file:.
+aiohappyeyeballs==2.6.1
+ # via aiohttp
+aiohttp==3.12.8
+ # via httpx-aiohttp
+ # via kernel
+aiosignal==1.3.2
+ # via aiohttp
annotated-types==0.6.0
# via pydantic
anyio==4.4.0
@@ -17,6 +24,10 @@ anyio==4.4.0
# via kernel
argcomplete==3.1.2
# via nox
+async-timeout==5.0.1
+ # via aiohttp
+attrs==25.3.0
+ # via aiohttp
certifi==2023.7.22
# via httpcore
# via httpx
@@ -34,16 +45,23 @@ execnet==2.1.1
# via pytest-xdist
filelock==3.12.4
# via virtualenv
+frozenlist==1.6.2
+ # via aiohttp
+ # via aiosignal
h11==0.14.0
# via httpcore
httpcore==1.0.2
# via httpx
httpx==0.28.1
+ # via httpx-aiohttp
# via kernel
# via respx
+httpx-aiohttp==0.1.6
+ # via kernel
idna==3.4
# via anyio
# via httpx
+ # via yarl
importlib-metadata==7.0.0
iniconfig==2.0.0
# via pytest
@@ -51,6 +69,9 @@ markdown-it-py==3.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
+multidict==6.4.4
+ # via aiohttp
+ # via yarl
mypy==1.14.1
mypy-extensions==1.0.0
# via mypy
@@ -65,6 +86,9 @@ platformdirs==3.11.0
# via virtualenv
pluggy==1.5.0
# via pytest
+propcache==0.3.1
+ # via aiohttp
+ # via yarl
pydantic==2.10.3
# via kernel
pydantic-core==2.27.1
@@ -98,11 +122,14 @@ tomli==2.0.2
typing-extensions==4.12.2
# via anyio
# via kernel
+ # via multidict
# via mypy
# via pydantic
# via pydantic-core
# via pyright
virtualenv==20.24.5
# via nox
+yarl==1.20.0
+ # via aiohttp
zipp==3.17.0
# via importlib-metadata
diff --git a/requirements.lock b/requirements.lock
index 4071919..4006aa2 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -10,11 +10,22 @@
# universal: false
-e file:.
+aiohappyeyeballs==2.6.1
+ # via aiohttp
+aiohttp==3.12.8
+ # via httpx-aiohttp
+ # via kernel
+aiosignal==1.3.2
+ # via aiohttp
annotated-types==0.6.0
# via pydantic
anyio==4.4.0
# via httpx
# via kernel
+async-timeout==5.0.1
+ # via aiohttp
+attrs==25.3.0
+ # via aiohttp
certifi==2023.7.22
# via httpcore
# via httpx
@@ -22,15 +33,28 @@ distro==1.8.0
# via kernel
exceptiongroup==1.2.2
# via anyio
+frozenlist==1.6.2
+ # via aiohttp
+ # via aiosignal
h11==0.14.0
# via httpcore
httpcore==1.0.2
# via httpx
httpx==0.28.1
+ # via httpx-aiohttp
+ # via kernel
+httpx-aiohttp==0.1.6
# via kernel
idna==3.4
# via anyio
# via httpx
+ # via yarl
+multidict==6.4.4
+ # via aiohttp
+ # via yarl
+propcache==0.3.1
+ # via aiohttp
+ # via yarl
pydantic==2.10.3
# via kernel
pydantic-core==2.27.1
@@ -41,5 +65,8 @@ sniffio==1.3.0
typing-extensions==4.12.2
# via anyio
# via kernel
+ # via multidict
# via pydantic
# via pydantic-core
+yarl==1.20.0
+ # via aiohttp
diff --git a/src/kernel/__init__.py b/src/kernel/__init__.py
index 8bc4875..03cd75c 100644
--- a/src/kernel/__init__.py
+++ b/src/kernel/__init__.py
@@ -37,7 +37,7 @@
UnprocessableEntityError,
APIResponseValidationError,
)
-from ._base_client import DefaultHttpxClient, DefaultAsyncHttpxClient
+from ._base_client import DefaultHttpxClient, DefaultAioHttpClient, DefaultAsyncHttpxClient
from ._utils._logs import setup_logging as _setup_logging
from .app_framework import (
App,
@@ -92,6 +92,7 @@
"DEFAULT_CONNECTION_LIMITS",
"DefaultHttpxClient",
"DefaultAsyncHttpxClient",
+ "DefaultAioHttpClient",
"KernelContext",
"KernelAction",
"KernelActionJson",
diff --git a/src/kernel/_base_client.py b/src/kernel/_base_client.py
index c86e919..c90f227 100644
--- a/src/kernel/_base_client.py
+++ b/src/kernel/_base_client.py
@@ -1289,6 +1289,24 @@ def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
+try:
+ import httpx_aiohttp
+except ImportError:
+
+ class _DefaultAioHttpClient(httpx.AsyncClient):
+ def __init__(self, **_kwargs: Any) -> None:
+ raise RuntimeError("To use the aiohttp client you must have installed the package with the `aiohttp` extra")
+else:
+
+ class _DefaultAioHttpClient(httpx_aiohttp.HttpxAiohttpClient): # type: ignore
+ def __init__(self, **kwargs: Any) -> None:
+ kwargs.setdefault("timeout", DEFAULT_TIMEOUT)
+ kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS)
+ kwargs.setdefault("follow_redirects", True)
+
+ super().__init__(**kwargs)
+
+
if TYPE_CHECKING:
DefaultAsyncHttpxClient = httpx.AsyncClient
"""An alias to `httpx.AsyncClient` that provides the same defaults that this SDK
@@ -1297,8 +1315,12 @@ def __init__(self, **kwargs: Any) -> None:
This is useful because overriding the `http_client` with your own instance of
`httpx.AsyncClient` will result in httpx's defaults being used, not ours.
"""
+
+ DefaultAioHttpClient = httpx.AsyncClient
+ """An alias to `httpx.AsyncClient` that changes the default HTTP transport to `aiohttp`."""
else:
DefaultAsyncHttpxClient = _DefaultAsyncHttpxClient
+ DefaultAioHttpClient = _DefaultAioHttpClient
class AsyncHttpxClientWrapper(DefaultAsyncHttpxClient):
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 23bc576..f175528 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.6.1" # x-release-please-version
+__version__ = "0.6.2" # x-release-please-version
diff --git a/src/kernel/resources/deployments.py b/src/kernel/resources/deployments.py
index 6442ff0..f27c894 100644
--- a/src/kernel/resources/deployments.py
+++ b/src/kernel/resources/deployments.py
@@ -7,7 +7,7 @@
import httpx
-from ..types import deployment_create_params
+from ..types import deployment_create_params, deployment_follow_params
from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes
from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
from .._compat import cached_property
@@ -150,6 +150,7 @@ def follow(
self,
id: str,
*,
+ since: str | 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,
@@ -163,6 +164,8 @@ def follow(
deployment reaches a terminal state.
Args:
+ since: Show logs since the given time (RFC timestamps or durations like 5m).
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -177,7 +180,11 @@ def follow(
return self._get(
f"/deployments/{id}/events",
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({"since": since}, deployment_follow_params.DeploymentFollowParams),
),
cast_to=cast(
Any, DeploymentFollowResponse
@@ -310,6 +317,7 @@ async def follow(
self,
id: str,
*,
+ since: str | 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,
@@ -323,6 +331,8 @@ async def follow(
deployment reaches a terminal state.
Args:
+ since: Show logs since the given time (RFC timestamps or durations like 5m).
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -337,7 +347,11 @@ async def follow(
return await self._get(
f"/deployments/{id}/events",
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({"since": since}, deployment_follow_params.DeploymentFollowParams),
),
cast_to=cast(
Any, DeploymentFollowResponse
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index cf2dbbc..c816cf1 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -2,7 +2,13 @@
from __future__ import annotations
-from .shared import LogEvent as LogEvent, ErrorEvent as ErrorEvent, ErrorModel as ErrorModel, ErrorDetail as ErrorDetail
+from .shared import (
+ LogEvent as LogEvent,
+ ErrorEvent as ErrorEvent,
+ ErrorModel as ErrorModel,
+ ErrorDetail as ErrorDetail,
+ HeartbeatEvent as HeartbeatEvent,
+)
from .app_list_params import AppListParams as AppListParams
from .app_list_response import AppListResponse as AppListResponse
from .browser_persistence import BrowserPersistence as BrowserPersistence
@@ -13,6 +19,7 @@
from .invocation_state_event import InvocationStateEvent as InvocationStateEvent
from .browser_create_response import BrowserCreateResponse as BrowserCreateResponse
from .deployment_create_params import DeploymentCreateParams as DeploymentCreateParams
+from .deployment_follow_params import DeploymentFollowParams as DeploymentFollowParams
from .invocation_create_params import InvocationCreateParams as InvocationCreateParams
from .invocation_update_params import InvocationUpdateParams as InvocationUpdateParams
from .browser_persistence_param import BrowserPersistenceParam as BrowserPersistenceParam
diff --git a/src/kernel/types/app_list_response.py b/src/kernel/types/app_list_response.py
index 1d35fd2..cf8463e 100644
--- a/src/kernel/types/app_list_response.py
+++ b/src/kernel/types/app_list_response.py
@@ -15,6 +15,9 @@ class AppListResponseItem(BaseModel):
app_name: str
"""Name of the application"""
+ deployment: str
+ """Deployment ID"""
+
region: Literal["aws.us-east-1a"]
"""Deployment region code"""
diff --git a/src/kernel/types/apps/deployment_follow_response.py b/src/kernel/types/apps/deployment_follow_response.py
index fae1b2b..6a19696 100644
--- a/src/kernel/types/apps/deployment_follow_response.py
+++ b/src/kernel/types/apps/deployment_follow_response.py
@@ -7,6 +7,7 @@
from ..._utils import PropertyInfo
from ..._models import BaseModel
from ..shared.log_event import LogEvent
+from ..shared.heartbeat_event import HeartbeatEvent
__all__ = ["DeploymentFollowResponse", "StateEvent", "StateUpdateEvent"]
@@ -36,5 +37,5 @@ class StateUpdateEvent(BaseModel):
DeploymentFollowResponse: TypeAlias = Annotated[
- Union[StateEvent, StateUpdateEvent, LogEvent], PropertyInfo(discriminator="event")
+ Union[StateEvent, StateUpdateEvent, LogEvent, HeartbeatEvent], PropertyInfo(discriminator="event")
]
diff --git a/src/kernel/types/deployment_follow_params.py b/src/kernel/types/deployment_follow_params.py
new file mode 100644
index 0000000..861f161
--- /dev/null
+++ b/src/kernel/types/deployment_follow_params.py
@@ -0,0 +1,12 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["DeploymentFollowParams"]
+
+
+class DeploymentFollowParams(TypedDict, total=False):
+ since: str
+ """Show logs since the given time (RFC timestamps or durations like 5m)."""
diff --git a/src/kernel/types/deployment_follow_response.py b/src/kernel/types/deployment_follow_response.py
index e95ce26..00f0685 100644
--- a/src/kernel/types/deployment_follow_response.py
+++ b/src/kernel/types/deployment_follow_response.py
@@ -9,6 +9,7 @@
from .shared.log_event import LogEvent
from .shared.error_event import ErrorEvent
from .deployment_state_event import DeploymentStateEvent
+from .shared.heartbeat_event import HeartbeatEvent
__all__ = ["DeploymentFollowResponse", "AppVersionSummaryEvent", "AppVersionSummaryEventAction"]
@@ -45,5 +46,6 @@ class AppVersionSummaryEvent(BaseModel):
DeploymentFollowResponse: TypeAlias = Annotated[
- Union[LogEvent, DeploymentStateEvent, AppVersionSummaryEvent, ErrorEvent], PropertyInfo(discriminator="event")
+ Union[LogEvent, DeploymentStateEvent, AppVersionSummaryEvent, ErrorEvent, HeartbeatEvent],
+ PropertyInfo(discriminator="event"),
]
diff --git a/src/kernel/types/invocation_follow_response.py b/src/kernel/types/invocation_follow_response.py
index e3d7e8e..2effbde 100644
--- a/src/kernel/types/invocation_follow_response.py
+++ b/src/kernel/types/invocation_follow_response.py
@@ -7,9 +7,10 @@
from .shared.log_event import LogEvent
from .shared.error_event import ErrorEvent
from .invocation_state_event import InvocationStateEvent
+from .shared.heartbeat_event import HeartbeatEvent
__all__ = ["InvocationFollowResponse"]
InvocationFollowResponse: TypeAlias = Annotated[
- Union[LogEvent, InvocationStateEvent, ErrorEvent], PropertyInfo(discriminator="event")
+ Union[LogEvent, InvocationStateEvent, ErrorEvent, HeartbeatEvent], PropertyInfo(discriminator="event")
]
diff --git a/src/kernel/types/shared/__init__.py b/src/kernel/types/shared/__init__.py
index e444e22..60a8de8 100644
--- a/src/kernel/types/shared/__init__.py
+++ b/src/kernel/types/shared/__init__.py
@@ -4,3 +4,4 @@
from .error_event import ErrorEvent as ErrorEvent
from .error_model import ErrorModel as ErrorModel
from .error_detail import ErrorDetail as ErrorDetail
+from .heartbeat_event import HeartbeatEvent as HeartbeatEvent
diff --git a/src/kernel/types/shared/heartbeat_event.py b/src/kernel/types/shared/heartbeat_event.py
new file mode 100644
index 0000000..d5ca811
--- /dev/null
+++ b/src/kernel/types/shared/heartbeat_event.py
@@ -0,0 +1,16 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from datetime import datetime
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+
+__all__ = ["HeartbeatEvent"]
+
+
+class HeartbeatEvent(BaseModel):
+ event: Literal["sse_heartbeat"]
+ """Event type identifier (always "sse_heartbeat")."""
+
+ timestamp: datetime
+ """Time the heartbeat was sent."""
diff --git a/tests/api_resources/apps/test_deployments.py b/tests/api_resources/apps/test_deployments.py
index 0a93c44..7b613c8 100644
--- a/tests/api_resources/apps/test_deployments.py
+++ b/tests/api_resources/apps/test_deployments.py
@@ -118,7 +118,9 @@ def test_path_params_follow(self, client: Kernel) -> None:
class TestAsyncDeployments:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
@pytest.mark.skip()
@parametrize
diff --git a/tests/api_resources/test_apps.py b/tests/api_resources/test_apps.py
index b902576..05066cd 100644
--- a/tests/api_resources/test_apps.py
+++ b/tests/api_resources/test_apps.py
@@ -56,7 +56,9 @@ def test_streaming_response_list(self, client: Kernel) -> None:
class TestAsyncApps:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
@pytest.mark.skip()
@parametrize
diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py
index 4593d2f..91a9429 100644
--- a/tests/api_resources/test_browsers.py
+++ b/tests/api_resources/test_browsers.py
@@ -213,7 +213,9 @@ def test_path_params_delete_by_id(self, client: Kernel) -> None:
class TestAsyncBrowsers:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
@pytest.mark.skip()
@parametrize
diff --git a/tests/api_resources/test_deployments.py b/tests/api_resources/test_deployments.py
index 4bd80fc..f68c9b0 100644
--- a/tests/api_resources/test_deployments.py
+++ b/tests/api_resources/test_deployments.py
@@ -9,7 +9,10 @@
from kernel import Kernel, AsyncKernel
from tests.utils import assert_matches_type
-from kernel.types import DeploymentCreateResponse, DeploymentRetrieveResponse
+from kernel.types import (
+ DeploymentCreateResponse,
+ DeploymentRetrieveResponse,
+)
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -115,7 +118,18 @@ def test_path_params_retrieve(self, client: Kernel) -> None:
@parametrize
def test_method_follow(self, client: Kernel) -> None:
deployment_stream = client.deployments.follow(
- "id",
+ id="id",
+ )
+ deployment_stream.response.close()
+
+ @pytest.mark.skip(
+ reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail"
+ )
+ @parametrize
+ def test_method_follow_with_all_params(self, client: Kernel) -> None:
+ deployment_stream = client.deployments.follow(
+ id="id",
+ since="2025-06-20T12:00:00Z",
)
deployment_stream.response.close()
@@ -125,7 +139,7 @@ def test_method_follow(self, client: Kernel) -> None:
@parametrize
def test_raw_response_follow(self, client: Kernel) -> None:
response = client.deployments.with_raw_response.follow(
- "id",
+ id="id",
)
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -138,7 +152,7 @@ def test_raw_response_follow(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_follow(self, client: Kernel) -> None:
with client.deployments.with_streaming_response.follow(
- "id",
+ id="id",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -155,12 +169,14 @@ def test_streaming_response_follow(self, client: Kernel) -> None:
def test_path_params_follow(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
client.deployments.with_raw_response.follow(
- "",
+ id="",
)
class TestAsyncDeployments:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
@pytest.mark.skip()
@parametrize
@@ -260,7 +276,18 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_follow(self, async_client: AsyncKernel) -> None:
deployment_stream = await async_client.deployments.follow(
- "id",
+ id="id",
+ )
+ await deployment_stream.response.aclose()
+
+ @pytest.mark.skip(
+ reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail"
+ )
+ @parametrize
+ async def test_method_follow_with_all_params(self, async_client: AsyncKernel) -> None:
+ deployment_stream = await async_client.deployments.follow(
+ id="id",
+ since="2025-06-20T12:00:00Z",
)
await deployment_stream.response.aclose()
@@ -270,7 +297,7 @@ async def test_method_follow(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_raw_response_follow(self, async_client: AsyncKernel) -> None:
response = await async_client.deployments.with_raw_response.follow(
- "id",
+ id="id",
)
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -283,7 +310,7 @@ async def test_raw_response_follow(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_follow(self, async_client: AsyncKernel) -> None:
async with async_client.deployments.with_streaming_response.follow(
- "id",
+ id="id",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -300,5 +327,5 @@ async def test_streaming_response_follow(self, async_client: AsyncKernel) -> Non
async def test_path_params_follow(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
await async_client.deployments.with_raw_response.follow(
- "",
+ id="",
)
diff --git a/tests/api_resources/test_invocations.py b/tests/api_resources/test_invocations.py
index 4fbd460..e739e44 100644
--- a/tests/api_resources/test_invocations.py
+++ b/tests/api_resources/test_invocations.py
@@ -264,7 +264,9 @@ def test_path_params_follow(self, client: Kernel) -> None:
class TestAsyncInvocations:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
@pytest.mark.skip()
@parametrize
diff --git a/tests/conftest.py b/tests/conftest.py
index 3a11d3f..c860af0 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -6,10 +6,12 @@
import logging
from typing import TYPE_CHECKING, Iterator, AsyncIterator
+import httpx
import pytest
from pytest_asyncio import is_async_test
-from kernel import Kernel, AsyncKernel
+from kernel import Kernel, AsyncKernel, DefaultAioHttpClient
+from kernel._utils import is_dict
if TYPE_CHECKING:
from _pytest.fixtures import FixtureRequest # pyright: ignore[reportPrivateImportUsage]
@@ -27,6 +29,19 @@ def pytest_collection_modifyitems(items: list[pytest.Function]) -> None:
for async_test in pytest_asyncio_tests:
async_test.add_marker(session_scope_marker, append=False)
+ # We skip tests that use both the aiohttp client and respx_mock as respx_mock
+ # doesn't support custom transports.
+ for item in items:
+ if "async_client" not in item.fixturenames or "respx_mock" not in item.fixturenames:
+ continue
+
+ if not hasattr(item, "callspec"):
+ continue
+
+ async_client_param = item.callspec.params.get("async_client")
+ if is_dict(async_client_param) and async_client_param.get("http_client") == "aiohttp":
+ item.add_marker(pytest.mark.skip(reason="aiohttp client is not compatible with respx_mock"))
+
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -45,9 +60,25 @@ def client(request: FixtureRequest) -> Iterator[Kernel]:
@pytest.fixture(scope="session")
async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncKernel]:
- strict = getattr(request, "param", True)
- if not isinstance(strict, bool):
- raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}")
-
- async with AsyncKernel(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client:
+ param = getattr(request, "param", True)
+
+ # defaults
+ strict = True
+ http_client: None | httpx.AsyncClient = None
+
+ if isinstance(param, bool):
+ strict = param
+ elif is_dict(param):
+ strict = param.get("strict", True)
+ assert isinstance(strict, bool)
+
+ http_client_type = param.get("http_client", "httpx")
+ if http_client_type == "aiohttp":
+ http_client = DefaultAioHttpClient()
+ else:
+ raise TypeError(f"Unexpected fixture parameter type {type(param)}, expected bool or dict")
+
+ async with AsyncKernel(
+ base_url=base_url, api_key=api_key, _strict_response_validation=strict, http_client=http_client
+ ) as client:
yield client
diff --git a/tests/test_client.py b/tests/test_client.py
index dea2c81..301dd58 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -191,6 +191,7 @@ def test_copy_signature(self) -> None:
copy_param = copy_signature.parameters.get(name)
assert copy_param is not None, f"copy() signature is missing the {name} param"
+ @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12")
def test_copy_build_request(self) -> None:
options = FinalRequestOptions(method="get", url="/foo")
@@ -1003,6 +1004,7 @@ def test_copy_signature(self) -> None:
copy_param = copy_signature.parameters.get(name)
assert copy_param is not None, f"copy() signature is missing the {name} param"
+ @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12")
def test_copy_build_request(self) -> None:
options = FinalRequestOptions(method="get", url="/foo")