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")