diff --git a/.release-please-manifest.json b/.release-please-manifest.json index f7014c3..e82003f 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/.stats.yml b/.stats.yml index 6ac19ba..7fb3d31 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-cb38560915edce03abce2ae3ef5bc745489dbe9b6f80c2b4ff42edf8c2ff276d.yml +openapi_spec_hash: a869194d6c864ba28d79ec0105439c3e +config_hash: ed56f95781ec9b2e73c97e1a66606071 diff --git a/CHANGELOG.md b/CHANGELOG.md index a6a6cec..5d1ad48 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/README.md b/README.md index 884d10c..beec3f0 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,83 @@ 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, +) + +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) + +# 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 7829585..56d852b 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/mypy.ini b/mypy.ini deleted file mode 100644 index 0745431..0000000 --- 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 f731ab3..5533264 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" @@ -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", ] @@ -157,6 +156,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" diff --git a/requirements-dev.lock b/requirements-dev.lock index 55681a9..56d0acc 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/src/kernel/_version.py b/src/kernel/_version.py index 1caec76..0e02001 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 diff --git a/src/kernel/pagination.py b/src/kernel/pagination.py new file mode 100644 index 0000000..cdf83c2 --- /dev/null +++ b/src/kernel/pagination.py @@ -0,0 +1,102 @@ +# 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, maybe_coerce_boolean, maybe_coerce_integer +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] + has_more: Optional[bool] = None + next_offset: Optional[int] = None + + @override + def _get_page_items(self) -> List[_T]: + items = self.items + if not items: + 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]: + next_offset = self.next_offset + if next_offset is None: + return None # type: ignore[unreachable] + + length = len(self._get_page_items()) + current_count = next_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}), + "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]: + items = self.items + if not items: + 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]: + next_offset = self.next_offset + if next_offset is None: + return None # type: ignore[unreachable] + + length = len(self._get_page_items()) + current_count = next_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}), + "has_more": maybe_coerce_boolean(response.headers.get("X-Has-More")), + "next_offset": maybe_coerce_integer(response.headers.get("X-Next-Offset")), + }, + ) diff --git a/src/kernel/resources/deployments.py b/src/kernel/resources/deployments.py index d54c4ec..5c7715d 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 @@ -151,13 +152,15 @@ def list( self, *, 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. # 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, + 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 05704a1..54124da 100644 --- a/src/kernel/types/deployment_list_params.py +++ b/src/kernel/types/deployment_list_params.py @@ -10,3 +10,9 @@ class DeploymentListParams(TypedDict, total=False): app_name: 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 ba7759d..d22b007 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 c177978..fc5d299 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") @@ -117,15 +118,17 @@ def test_path_params_retrieve(self, client: Kernel) -> None: @parametrize def test_method_list(self, client: Kernel) -> None: deployment = client.deployments.list() - 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_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 @@ -135,7 +138,7 @@ def test_raw_response_list(self, client: Kernel) -> None: 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 @@ -145,7 +148,7 @@ def test_streaming_response_list(self, client: Kernel) -> None: 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 @@ -301,15 +304,17 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: @parametrize async def test_method_list(self, async_client: AsyncKernel) -> None: deployment = await async_client.deployments.list() - 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_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 @@ -319,7 +324,7 @@ async def test_raw_response_list(self, async_client: AsyncKernel) -> None: 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 @@ -329,7 +334,7 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: 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 diff --git a/tests/test_client.py b/tests/test_client.py index d5a1784..74ffb05 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