From fd28518bfcc27fca2b86f48da4a558ddbcc4be6c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 10 Jun 2025 19:59:17 +0000 Subject: [PATCH 01/16] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index b6ad26b1..f54d0d07 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 25 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/nen-labs%2Fsteel-d5a9cdf80ea29558c755cdebab1f876a01bbb6f6e7cd480b677b8b8313d6e92c.yml -openapi_spec_hash: ac6ed4b57809802e0924cdf97ca92102 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/nen-labs%2Fsteel-e788c6b435e0200ec2804c2ba57649047f71029b0e4daeae13876bb4636829a9.yml +openapi_spec_hash: 80b61a9157af36229eec5091cbe5e1e0 config_hash: 8fee9983d818d458fca9da01d2013f05 From 825c97fc4b37a582267bf338b2640de17a254546 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 13 Jun 2025 02:16:29 +0000 Subject: [PATCH 02/16] chore(tests): run tests in parallel --- pyproject.toml | 3 ++- requirements-dev.lock | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f4c5778a..cc91a196 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ dev-dependencies = [ "importlib-metadata>=6.7.0", "rich>=13.7.1", "nest_asyncio==1.6.0", + "pytest-xdist>=3.6.1", ] [tool.rye.scripts] @@ -125,7 +126,7 @@ replacement = '[\1](https://github.com/steel-dev/steel-python/tree/main/\g<2>)' [tool.pytest.ini_options] testpaths = ["tests"] -addopts = "--tb=short" +addopts = "--tb=short -n auto" xfail_strict = true asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "session" diff --git a/requirements-dev.lock b/requirements-dev.lock index 10d7fdb0..ce76f286 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -30,6 +30,8 @@ distro==1.8.0 exceptiongroup==1.2.2 # via anyio # via pytest +execnet==2.1.1 + # via pytest-xdist filelock==3.12.4 # via virtualenv h11==0.14.0 @@ -72,7 +74,9 @@ pygments==2.18.0 pyright==1.1.399 pytest==8.3.3 # via pytest-asyncio + # via pytest-xdist pytest-asyncio==0.24.0 +pytest-xdist==3.7.0 python-dateutil==2.8.2 # via time-machine pytz==2023.3.post1 From 96bc0a57989271ac57a5e916a9678798e8bc7123 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 13 Jun 2025 02:40:52 +0000 Subject: [PATCH 03/16] fix(client): correctly parse binary response | stream --- src/steel/_base_client.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/steel/_base_client.py b/src/steel/_base_client.py index 9d781f16..5ca39a24 100644 --- a/src/steel/_base_client.py +++ b/src/steel/_base_client.py @@ -1071,7 +1071,14 @@ def _process_response( ) -> ResponseT: origin = get_origin(cast_to) or cast_to - if inspect.isclass(origin) and issubclass(origin, BaseAPIResponse): + if ( + inspect.isclass(origin) + and issubclass(origin, BaseAPIResponse) + # we only want to actually return the custom BaseAPIResponse class if we're + # returning the raw response, or if we're not streaming SSE, as if we're streaming + # SSE then `cast_to` doesn't actively reflect the type we need to parse into + and (not stream or bool(response.request.headers.get(RAW_RESPONSE_HEADER))) + ): if not issubclass(origin, APIResponse): raise TypeError(f"API Response types must subclass {APIResponse}; Received {origin}") @@ -1574,7 +1581,14 @@ async def _process_response( ) -> ResponseT: origin = get_origin(cast_to) or cast_to - if inspect.isclass(origin) and issubclass(origin, BaseAPIResponse): + if ( + inspect.isclass(origin) + and issubclass(origin, BaseAPIResponse) + # we only want to actually return the custom BaseAPIResponse class if we're + # returning the raw response, or if we're not streaming SSE, as if we're streaming + # SSE then `cast_to` doesn't actively reflect the type we need to parse into + and (not stream or bool(response.request.headers.get(RAW_RESPONSE_HEADER))) + ): if not issubclass(origin, AsyncAPIResponse): raise TypeError(f"API Response types must subclass {AsyncAPIResponse}; Received {origin}") From ad4f8a1256746d265dd9a2c0a9bfca53fa4c61f3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 02:48:15 +0000 Subject: [PATCH 04/16] chore(tests): add tests for httpx client instantiation & proxies --- tests/test_client.py | 53 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/tests/test_client.py b/tests/test_client.py index f8730cae..a231a980 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -27,7 +27,14 @@ from steel._models import BaseModel, FinalRequestOptions from steel._constants import RAW_RESPONSE_HEADER from steel._exceptions import APIStatusError, APITimeoutError, APIResponseValidationError -from steel._base_client import DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, BaseClient, make_request_options +from steel._base_client import ( + DEFAULT_TIMEOUT, + HTTPX_DEFAULT_TIMEOUT, + BaseClient, + DefaultHttpxClient, + DefaultAsyncHttpxClient, + make_request_options, +) from steel.types.session_create_params import SessionCreateParams from .utils import update_env @@ -841,6 +848,28 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: assert response.http_request.headers.get("x-stainless-retry-count") == "42" + def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: + # Test that the proxy environment variables are set correctly + monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + + client = DefaultHttpxClient() + + mounts = tuple(client._mounts.items()) + assert len(mounts) == 1 + assert mounts[0][0].pattern == "https://" + + @pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning") + def test_default_client_creation(self) -> None: + # Ensure that the client can be initialized without any exceptions + DefaultHttpxClient( + verify=True, + cert=None, + trust_env=True, + http1=True, + http2=False, + limits=httpx.Limits(max_connections=100, max_keepalive_connections=20), + ) + @pytest.mark.respx(base_url=base_url) def test_follow_redirects(self, respx_mock: MockRouter) -> None: # Test that the default follow_redirects=True allows following redirects @@ -1712,6 +1741,28 @@ async def test_main() -> None: time.sleep(0.1) + async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: + # Test that the proxy environment variables are set correctly + monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + + client = DefaultAsyncHttpxClient() + + mounts = tuple(client._mounts.items()) + assert len(mounts) == 1 + assert mounts[0][0].pattern == "https://" + + @pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning") + async def test_default_client_creation(self) -> None: + # Ensure that the client can be initialized without any exceptions + DefaultAsyncHttpxClient( + verify=True, + cert=None, + trust_env=True, + http1=True, + http2=False, + limits=httpx.Limits(max_connections=100, max_keepalive_connections=20), + ) + @pytest.mark.respx(base_url=base_url) async def test_follow_redirects(self, respx_mock: MockRouter) -> None: # Test that the default follow_redirects=True allows following redirects From 95540f26791852e6a9864e4ebe12fb8762ed0241 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 04:17:41 +0000 Subject: [PATCH 05/16] chore(internal): update conftest.py --- tests/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 7434ff13..34ebef34 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,5 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + from __future__ import annotations import os From 4f581f14fac853d9ffb09aac7efcd4cba8e7e008 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 06:48:46 +0000 Subject: [PATCH 06/16] chore(ci): enable for pull requests --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c0dbaed..52281f3e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,10 @@ on: - 'integrated/**' - 'stl-preview-head/**' - 'stl-preview-base/**' + pull_request: + branches-ignore: + - 'stl-preview-head/**' + - 'stl-preview-base/**' jobs: lint: From e210ee78b18dc19b9b39b67c2b37f16c855f0c58 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 02:20:28 +0000 Subject: [PATCH 07/16] chore(readme): update badges --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ecff035e..733ea31f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Steel Python API library -[![PyPI version](https://img.shields.io/pypi/v/steel-sdk.svg)](https://pypi.org/project/steel-sdk/) +[![PyPI version]()](https://pypi.org/project/steel-sdk/) The Steel Python library provides convenient access to the Steel REST API from any Python 3.8+ application. The library includes type definitions for all request params and response fields, From ffacf10a70b267665f65ee31aa176a2d0fdb0fca Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 05:56:40 +0000 Subject: [PATCH 08/16] fix(tests): fix: tests which call HTTP endpoints directly with the example parameters --- tests/test_client.py | 41 ++++++++--------------------------------- 1 file changed, 8 insertions(+), 33 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index a231a980..9ec01a8d 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -23,9 +23,7 @@ from steel import Steel, AsyncSteel, APIResponseValidationError from steel._types import Omit -from steel._utils import maybe_transform from steel._models import BaseModel, FinalRequestOptions -from steel._constants import RAW_RESPONSE_HEADER from steel._exceptions import APIStatusError, APITimeoutError, APIResponseValidationError from steel._base_client import ( DEFAULT_TIMEOUT, @@ -35,7 +33,6 @@ DefaultAsyncHttpxClient, make_request_options, ) -from steel.types.session_create_params import SessionCreateParams from .utils import update_env @@ -745,32 +742,21 @@ def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str @mock.patch("steel._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: + def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: Steel) -> None: respx_mock.post("/v1/sessions").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - self.client.post( - "/v1/sessions", - body=cast(object, maybe_transform({}, SessionCreateParams)), - cast_to=httpx.Response, - options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, - ) + client.sessions.with_streaming_response.create().__enter__() assert _get_open_connections(self.client) == 0 @mock.patch("steel._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: + def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: Steel) -> None: respx_mock.post("/v1/sessions").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - self.client.post( - "/v1/sessions", - body=cast(object, maybe_transform({}, SessionCreateParams)), - cast_to=httpx.Response, - options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, - ) - + client.sessions.with_streaming_response.create().__enter__() assert _get_open_connections(self.client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -1588,32 +1574,21 @@ async def test_parse_retry_after_header(self, remaining_retries: int, retry_afte @mock.patch("steel._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: + async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, async_client: AsyncSteel) -> None: respx_mock.post("/v1/sessions").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - await self.client.post( - "/v1/sessions", - body=cast(object, maybe_transform({}, SessionCreateParams)), - cast_to=httpx.Response, - options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, - ) + await async_client.sessions.with_streaming_response.create().__aenter__() assert _get_open_connections(self.client) == 0 @mock.patch("steel._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: + async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, async_client: AsyncSteel) -> None: respx_mock.post("/v1/sessions").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - await self.client.post( - "/v1/sessions", - body=cast(object, maybe_transform({}, SessionCreateParams)), - cast_to=httpx.Response, - options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, - ) - + await async_client.sessions.with_streaming_response.create().__aenter__() assert _get_open_connections(self.client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) From 2c5ba6f2f450b6b7a2172ee9dfcf9de93426773b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 19 Jun 2025 02:58:13 +0000 Subject: [PATCH 09/16] docs(client): fix httpx.Timeout documentation reference --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 733ea31f..84b05a4e 100644 --- a/README.md +++ b/README.md @@ -224,7 +224,7 @@ client.with_options(max_retries=5).sessions.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 steel import Steel From 87bc6e2420ea32f8336251fdda4c8353b8181f35 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 21 Jun 2025 04:18:52 +0000 Subject: [PATCH 10/16] feat(client): add support for aiohttp --- README.md | 33 +++++++++++++++++ pyproject.toml | 2 + requirements-dev.lock | 27 ++++++++++++++ requirements.lock | 27 ++++++++++++++ src/steel/__init__.py | 3 +- src/steel/_base_client.py | 22 +++++++++++ tests/api_resources/sessions/test_files.py | 4 +- tests/api_resources/test_client.py | 4 +- tests/api_resources/test_credentials.py | 4 +- tests/api_resources/test_files.py | 4 +- tests/api_resources/test_sessions.py | 4 +- tests/conftest.py | 43 +++++++++++++++++++--- 12 files changed, 165 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 84b05a4e..c258968d 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,39 @@ 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 steel-sdk[aiohttp] +``` + +Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: + +```python +import asyncio +from steel import DefaultAioHttpClient +from steel import AsyncSteel + + +async def main() -> None: + async with AsyncSteel( + http_client=DefaultAioHttpClient(), + ) as client: + session = await client.sessions.create( + api_timeout=20000, + use_proxy=True, + ) + print(session.id) + + +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: diff --git a/pyproject.toml b/pyproject.toml index cc91a196..7e594f3b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,8 @@ classifiers = [ Homepage = "https://github.com/steel-dev/steel-python" Repository = "https://github.com/steel-dev/steel-python" +[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 ce76f286..6738edc6 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 steel-sdk +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 steel-sdk 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 respx # via steel-sdk +httpx-aiohttp==0.1.6 + # via steel-sdk 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 steel-sdk pydantic-core==2.27.1 @@ -97,6 +121,7 @@ tomli==2.0.2 # via pytest typing-extensions==4.12.2 # via anyio + # via multidict # via mypy # via pydantic # via pydantic-core @@ -104,5 +129,7 @@ typing-extensions==4.12.2 # via steel-sdk 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 79dbd2b6..8a0fc2ec 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 steel-sdk +aiosignal==1.3.2 + # via aiohttp annotated-types==0.6.0 # via pydantic anyio==4.4.0 # via httpx # via steel-sdk +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 steel-sdk 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 steel-sdk +httpx-aiohttp==0.1.6 # via steel-sdk 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 steel-sdk pydantic-core==2.27.1 @@ -40,6 +64,9 @@ sniffio==1.3.0 # via steel-sdk typing-extensions==4.12.2 # via anyio + # via multidict # via pydantic # via pydantic-core # via steel-sdk +yarl==1.20.0 + # via aiohttp diff --git a/src/steel/__init__.py b/src/steel/__init__.py index 1a873730..132f80e6 100644 --- a/src/steel/__init__.py +++ b/src/steel/__init__.py @@ -26,7 +26,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 __all__ = [ @@ -68,6 +68,7 @@ "DEFAULT_CONNECTION_LIMITS", "DefaultHttpxClient", "DefaultAsyncHttpxClient", + "DefaultAioHttpClient", ] if not _t.TYPE_CHECKING: diff --git a/src/steel/_base_client.py b/src/steel/_base_client.py index 5ca39a24..74d7c57e 100644 --- a/src/steel/_base_client.py +++ b/src/steel/_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/tests/api_resources/sessions/test_files.py b/tests/api_resources/sessions/test_files.py index 8537aab5..66f22996 100644 --- a/tests/api_resources/sessions/test_files.py +++ b/tests/api_resources/sessions/test_files.py @@ -308,7 +308,9 @@ def test_path_params_upload(self, client: Steel) -> None: class TestAsyncFiles: - 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"] + ) @parametrize async def test_method_list(self, async_client: AsyncSteel) -> None: diff --git a/tests/api_resources/test_client.py b/tests/api_resources/test_client.py index 2e2f94a8..3ec47d44 100644 --- a/tests/api_resources/test_client.py +++ b/tests/api_resources/test_client.py @@ -147,7 +147,9 @@ def test_streaming_response_screenshot(self, client: Steel) -> None: class TestAsyncClient: - 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"] + ) @parametrize async def test_method_pdf(self, async_client: AsyncSteel) -> None: diff --git a/tests/api_resources/test_credentials.py b/tests/api_resources/test_credentials.py index ec79d2fe..97abc5f4 100644 --- a/tests/api_resources/test_credentials.py +++ b/tests/api_resources/test_credentials.py @@ -172,7 +172,9 @@ def test_streaming_response_delete(self, client: Steel) -> None: class TestAsyncCredentials: - 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"] + ) @parametrize async def test_method_create(self, async_client: AsyncSteel) -> None: diff --git a/tests/api_resources/test_files.py b/tests/api_resources/test_files.py index 8c42148b..a0570325 100644 --- a/tests/api_resources/test_files.py +++ b/tests/api_resources/test_files.py @@ -173,7 +173,9 @@ def test_streaming_response_upload(self, client: Steel) -> None: class TestAsyncFiles: - 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"] + ) @parametrize async def test_method_list(self, async_client: AsyncSteel) -> None: diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 9bdb8d49..00c54595 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -386,7 +386,9 @@ def test_streaming_response_release_all(self, client: Steel) -> None: class TestAsyncSessions: - 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"] + ) @parametrize async def test_method_create(self, async_client: AsyncSteel) -> None: diff --git a/tests/conftest.py b/tests/conftest.py index 34ebef34..126886c9 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 steel import Steel, AsyncSteel +from steel import Steel, AsyncSteel, DefaultAioHttpClient +from steel._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[Steel]: @pytest.fixture(scope="session") async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncSteel]: - strict = getattr(request, "param", True) - if not isinstance(strict, bool): - raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") - - async with AsyncSteel(base_url=base_url, steel_api_key=steel_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 AsyncSteel( + base_url=base_url, steel_api_key=steel_api_key, _strict_response_validation=strict, http_client=http_client + ) as client: yield client From c64047c3036b3054f1f466f7521db5f76d013c8d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Jun 2025 04:26:50 +0000 Subject: [PATCH 11/16] chore(tests): skip some failing tests on the latest python versions --- tests/test_client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index 9ec01a8d..6e2b94f5 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -197,6 +197,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") @@ -1025,6 +1026,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") From 978ad9cb86523cf783ca1c059b137d497422d626 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Jun 2025 13:19:29 +0000 Subject: [PATCH 12/16] feat(api): api update --- .stats.yml | 6 +- src/steel/_client.py | 18 ++-- tests/conftest.py | 8 +- tests/test_client.py | 223 ++++++++----------------------------------- 4 files changed, 54 insertions(+), 201 deletions(-) diff --git a/.stats.yml b/.stats.yml index f54d0d07..d612d691 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 25 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/nen-labs%2Fsteel-e788c6b435e0200ec2804c2ba57649047f71029b0e4daeae13876bb4636829a9.yml -openapi_spec_hash: 80b61a9157af36229eec5091cbe5e1e0 -config_hash: 8fee9983d818d458fca9da01d2013f05 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/nen-labs%2Fsteel-f81cef6f87adc0530d9bc21e8d47c95aadef9d5bc3c282837ae0ad41d7f71bac.yml +openapi_spec_hash: 5b273b225abb80a969ea1485bf399745 +config_hash: 42515bf83f1e0e765071038fcf702122 diff --git a/src/steel/_client.py b/src/steel/_client.py index cc3fa3c3..326b6e18 100644 --- a/src/steel/_client.py +++ b/src/steel/_client.py @@ -38,7 +38,7 @@ ) from .resources import files, credentials from ._streaming import Stream as Stream, AsyncStream as AsyncStream -from ._exceptions import SteelError, APIStatusError +from ._exceptions import APIStatusError from ._base_client import ( DEFAULT_MAX_RETRIES, SyncAPIClient, @@ -61,7 +61,7 @@ class Steel(SyncAPIClient): with_streaming_response: SteelWithStreamedResponse # client options - steel_api_key: str + steel_api_key: str | None def __init__( self, @@ -92,10 +92,6 @@ def __init__( """ if steel_api_key is None: steel_api_key = os.environ.get("STEEL_API_KEY") - if steel_api_key is None: - raise SteelError( - "The steel_api_key client option must be set either by passing steel_api_key to the client or by setting the STEEL_API_KEY environment variable" - ) self.steel_api_key = steel_api_key if base_url is None: @@ -129,6 +125,8 @@ def qs(self) -> Querystring: @override def auth_headers(self) -> dict[str, str]: steel_api_key = self.steel_api_key + if steel_api_key is None: + return {} return {"steel-api-key": steel_api_key} @property @@ -390,7 +388,7 @@ class AsyncSteel(AsyncAPIClient): with_streaming_response: AsyncSteelWithStreamedResponse # client options - steel_api_key: str + steel_api_key: str | None def __init__( self, @@ -421,10 +419,6 @@ def __init__( """ if steel_api_key is None: steel_api_key = os.environ.get("STEEL_API_KEY") - if steel_api_key is None: - raise SteelError( - "The steel_api_key client option must be set either by passing steel_api_key to the client or by setting the STEEL_API_KEY environment variable" - ) self.steel_api_key = steel_api_key if base_url is None: @@ -458,6 +452,8 @@ def qs(self) -> Querystring: @override def auth_headers(self) -> dict[str, str]: steel_api_key = self.steel_api_key + if steel_api_key is None: + return {} return {"steel-api-key": steel_api_key} @property diff --git a/tests/conftest.py b/tests/conftest.py index 126886c9..14e2cb5f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -45,8 +45,6 @@ def pytest_collection_modifyitems(items: list[pytest.Function]) -> None: base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") -steel_api_key = "My Steel API Key" - @pytest.fixture(scope="session") def client(request: FixtureRequest) -> Iterator[Steel]: @@ -54,7 +52,7 @@ def client(request: FixtureRequest) -> Iterator[Steel]: if not isinstance(strict, bool): raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") - with Steel(base_url=base_url, steel_api_key=steel_api_key, _strict_response_validation=strict) as client: + with Steel(base_url=base_url, _strict_response_validation=strict) as client: yield client @@ -78,7 +76,5 @@ async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncSteel]: else: raise TypeError(f"Unexpected fixture parameter type {type(param)}, expected bool or dict") - async with AsyncSteel( - base_url=base_url, steel_api_key=steel_api_key, _strict_response_validation=strict, http_client=http_client - ) as client: + async with AsyncSteel(base_url=base_url, _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 6e2b94f5..0c16cdb5 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -37,7 +37,6 @@ from .utils import update_env base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") -steel_api_key = "My Steel API Key" def _get_params(client: BaseClient[Any, Any]) -> dict[str, str]: @@ -59,7 +58,7 @@ def _get_open_connections(client: Steel | AsyncSteel) -> int: class TestSteel: - client = Steel(base_url=base_url, steel_api_key=steel_api_key, _strict_response_validation=True) + client = Steel(base_url=base_url, _strict_response_validation=True) @pytest.mark.respx(base_url=base_url) def test_raw_response(self, respx_mock: MockRouter) -> None: @@ -85,10 +84,6 @@ def test_copy(self) -> None: copied = self.client.copy() assert id(copied) != id(self.client) - copied = self.client.copy(steel_api_key="another My Steel API Key") - assert copied.steel_api_key == "another My Steel API Key" - assert self.client.steel_api_key == "My Steel API Key" - def test_copy_default_options(self) -> None: # options that have a default are overridden correctly copied = self.client.copy(max_retries=7) @@ -106,12 +101,7 @@ def test_copy_default_options(self) -> None: assert isinstance(self.client.timeout, httpx.Timeout) def test_copy_default_headers(self) -> None: - client = Steel( - base_url=base_url, - steel_api_key=steel_api_key, - _strict_response_validation=True, - default_headers={"X-Foo": "bar"}, - ) + client = Steel(base_url=base_url, _strict_response_validation=True, default_headers={"X-Foo": "bar"}) assert client.default_headers["X-Foo"] == "bar" # does not override the already given value when not specified @@ -143,12 +133,7 @@ def test_copy_default_headers(self) -> None: client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"}) def test_copy_default_query(self) -> None: - client = Steel( - base_url=base_url, - steel_api_key=steel_api_key, - _strict_response_validation=True, - default_query={"foo": "bar"}, - ) + client = Steel(base_url=base_url, _strict_response_validation=True, default_query={"foo": "bar"}) assert _get_params(client)["foo"] == "bar" # does not override the already given value when not specified @@ -272,9 +257,7 @@ def test_request_timeout(self) -> None: assert timeout == httpx.Timeout(100.0) def test_client_timeout_option(self) -> None: - client = Steel( - base_url=base_url, steel_api_key=steel_api_key, _strict_response_validation=True, timeout=httpx.Timeout(0) - ) + client = Steel(base_url=base_url, _strict_response_validation=True, timeout=httpx.Timeout(0)) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore @@ -283,12 +266,7 @@ def test_client_timeout_option(self) -> None: def test_http_client_timeout_option(self) -> None: # custom timeout given to the httpx client should be used with httpx.Client(timeout=None) as http_client: - client = Steel( - base_url=base_url, - steel_api_key=steel_api_key, - _strict_response_validation=True, - http_client=http_client, - ) + client = Steel(base_url=base_url, _strict_response_validation=True, http_client=http_client) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore @@ -296,12 +274,7 @@ def test_http_client_timeout_option(self) -> None: # no timeout given to the httpx client should not use the httpx default with httpx.Client() as http_client: - client = Steel( - base_url=base_url, - steel_api_key=steel_api_key, - _strict_response_validation=True, - http_client=http_client, - ) + client = Steel(base_url=base_url, _strict_response_validation=True, http_client=http_client) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore @@ -309,12 +282,7 @@ def test_http_client_timeout_option(self) -> None: # explicitly passing the default timeout currently results in it being ignored with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: - client = Steel( - base_url=base_url, - steel_api_key=steel_api_key, - _strict_response_validation=True, - http_client=http_client, - ) + client = Steel(base_url=base_url, _strict_response_validation=True, http_client=http_client) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore @@ -323,27 +291,16 @@ def test_http_client_timeout_option(self) -> None: async def test_invalid_http_client(self) -> None: with pytest.raises(TypeError, match="Invalid `http_client` arg"): async with httpx.AsyncClient() as http_client: - Steel( - base_url=base_url, - steel_api_key=steel_api_key, - _strict_response_validation=True, - http_client=cast(Any, http_client), - ) + Steel(base_url=base_url, _strict_response_validation=True, http_client=cast(Any, http_client)) def test_default_headers_option(self) -> None: - client = Steel( - base_url=base_url, - steel_api_key=steel_api_key, - _strict_response_validation=True, - default_headers={"X-Foo": "bar"}, - ) + client = Steel(base_url=base_url, _strict_response_validation=True, default_headers={"X-Foo": "bar"}) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "bar" assert request.headers.get("x-stainless-lang") == "python" client2 = Steel( base_url=base_url, - steel_api_key=steel_api_key, _strict_response_validation=True, default_headers={ "X-Foo": "stainless", @@ -355,12 +312,7 @@ def test_default_headers_option(self) -> None: assert request.headers.get("x-stainless-lang") == "my-overriding-header" def test_default_query_option(self) -> None: - client = Steel( - base_url=base_url, - steel_api_key=steel_api_key, - _strict_response_validation=True, - default_query={"query_param": "bar"}, - ) + client = Steel(base_url=base_url, _strict_response_validation=True, default_query={"query_param": "bar"}) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) url = httpx.URL(request.url) assert dict(url.params) == {"query_param": "bar"} @@ -559,9 +511,7 @@ class Model(BaseModel): assert response.foo == 2 def test_base_url_setter(self) -> None: - client = Steel( - base_url="https://example.com/from_init", steel_api_key=steel_api_key, _strict_response_validation=True - ) + client = Steel(base_url="https://example.com/from_init", _strict_response_validation=True) assert client.base_url == "https://example.com/from_init/" client.base_url = "https://example.com/from_setter" # type: ignore[assignment] @@ -570,20 +520,15 @@ def test_base_url_setter(self) -> None: def test_base_url_env(self) -> None: with update_env(STEEL_BASE_URL="http://localhost:5000/from/env"): - client = Steel(steel_api_key=steel_api_key, _strict_response_validation=True) + client = Steel(_strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" @pytest.mark.parametrize( "client", [ + Steel(base_url="http://localhost:5000/custom/path/", _strict_response_validation=True), Steel( base_url="http://localhost:5000/custom/path/", - steel_api_key=steel_api_key, - _strict_response_validation=True, - ), - Steel( - base_url="http://localhost:5000/custom/path/", - steel_api_key=steel_api_key, _strict_response_validation=True, http_client=httpx.Client(), ), @@ -603,14 +548,9 @@ def test_base_url_trailing_slash(self, client: Steel) -> None: @pytest.mark.parametrize( "client", [ + Steel(base_url="http://localhost:5000/custom/path/", _strict_response_validation=True), Steel( base_url="http://localhost:5000/custom/path/", - steel_api_key=steel_api_key, - _strict_response_validation=True, - ), - Steel( - base_url="http://localhost:5000/custom/path/", - steel_api_key=steel_api_key, _strict_response_validation=True, http_client=httpx.Client(), ), @@ -630,14 +570,9 @@ def test_base_url_no_trailing_slash(self, client: Steel) -> None: @pytest.mark.parametrize( "client", [ + Steel(base_url="http://localhost:5000/custom/path/", _strict_response_validation=True), Steel( base_url="http://localhost:5000/custom/path/", - steel_api_key=steel_api_key, - _strict_response_validation=True, - ), - Steel( - base_url="http://localhost:5000/custom/path/", - steel_api_key=steel_api_key, _strict_response_validation=True, http_client=httpx.Client(), ), @@ -655,7 +590,7 @@ def test_absolute_request_url(self, client: Steel) -> None: assert request.url == "https://myapi.com/foo" def test_copied_client_does_not_close_http(self) -> None: - client = Steel(base_url=base_url, steel_api_key=steel_api_key, _strict_response_validation=True) + client = Steel(base_url=base_url, _strict_response_validation=True) assert not client.is_closed() copied = client.copy() @@ -666,7 +601,7 @@ def test_copied_client_does_not_close_http(self) -> None: assert not client.is_closed() def test_client_context_manager(self) -> None: - client = Steel(base_url=base_url, steel_api_key=steel_api_key, _strict_response_validation=True) + client = Steel(base_url=base_url, _strict_response_validation=True) with client as c2: assert c2 is client assert not c2.is_closed() @@ -687,12 +622,7 @@ class Model(BaseModel): def test_client_max_retries_validation(self) -> None: with pytest.raises(TypeError, match=r"max_retries cannot be None"): - Steel( - base_url=base_url, - steel_api_key=steel_api_key, - _strict_response_validation=True, - max_retries=cast(Any, None), - ) + Steel(base_url=base_url, _strict_response_validation=True, max_retries=cast(Any, None)) @pytest.mark.respx(base_url=base_url) def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: @@ -701,12 +631,12 @@ class Model(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) - strict_client = Steel(base_url=base_url, steel_api_key=steel_api_key, _strict_response_validation=True) + strict_client = Steel(base_url=base_url, _strict_response_validation=True) with pytest.raises(APIResponseValidationError): strict_client.get("/foo", cast_to=Model) - client = Steel(base_url=base_url, steel_api_key=steel_api_key, _strict_response_validation=False) + client = Steel(base_url=base_url, _strict_response_validation=False) response = client.get("/foo", cast_to=Model) assert isinstance(response, str) # type: ignore[unreachable] @@ -734,7 +664,7 @@ class Model(BaseModel): ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None: - client = Steel(base_url=base_url, steel_api_key=steel_api_key, _strict_response_validation=True) + client = Steel(base_url=base_url, _strict_response_validation=True) headers = httpx.Headers({"retry-after": retry_after}) options = FinalRequestOptions(method="get", url="/foo", max_retries=3) @@ -886,7 +816,7 @@ def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None: class TestAsyncSteel: - client = AsyncSteel(base_url=base_url, steel_api_key=steel_api_key, _strict_response_validation=True) + client = AsyncSteel(base_url=base_url, _strict_response_validation=True) @pytest.mark.respx(base_url=base_url) @pytest.mark.asyncio @@ -914,10 +844,6 @@ def test_copy(self) -> None: copied = self.client.copy() assert id(copied) != id(self.client) - copied = self.client.copy(steel_api_key="another My Steel API Key") - assert copied.steel_api_key == "another My Steel API Key" - assert self.client.steel_api_key == "My Steel API Key" - def test_copy_default_options(self) -> None: # options that have a default are overridden correctly copied = self.client.copy(max_retries=7) @@ -935,12 +861,7 @@ def test_copy_default_options(self) -> None: assert isinstance(self.client.timeout, httpx.Timeout) def test_copy_default_headers(self) -> None: - client = AsyncSteel( - base_url=base_url, - steel_api_key=steel_api_key, - _strict_response_validation=True, - default_headers={"X-Foo": "bar"}, - ) + client = AsyncSteel(base_url=base_url, _strict_response_validation=True, default_headers={"X-Foo": "bar"}) assert client.default_headers["X-Foo"] == "bar" # does not override the already given value when not specified @@ -972,12 +893,7 @@ def test_copy_default_headers(self) -> None: client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"}) def test_copy_default_query(self) -> None: - client = AsyncSteel( - base_url=base_url, - steel_api_key=steel_api_key, - _strict_response_validation=True, - default_query={"foo": "bar"}, - ) + client = AsyncSteel(base_url=base_url, _strict_response_validation=True, default_query={"foo": "bar"}) assert _get_params(client)["foo"] == "bar" # does not override the already given value when not specified @@ -1101,9 +1017,7 @@ async def test_request_timeout(self) -> None: assert timeout == httpx.Timeout(100.0) async def test_client_timeout_option(self) -> None: - client = AsyncSteel( - base_url=base_url, steel_api_key=steel_api_key, _strict_response_validation=True, timeout=httpx.Timeout(0) - ) + client = AsyncSteel(base_url=base_url, _strict_response_validation=True, timeout=httpx.Timeout(0)) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore @@ -1112,12 +1026,7 @@ async def test_client_timeout_option(self) -> None: async def test_http_client_timeout_option(self) -> None: # custom timeout given to the httpx client should be used async with httpx.AsyncClient(timeout=None) as http_client: - client = AsyncSteel( - base_url=base_url, - steel_api_key=steel_api_key, - _strict_response_validation=True, - http_client=http_client, - ) + client = AsyncSteel(base_url=base_url, _strict_response_validation=True, http_client=http_client) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore @@ -1125,12 +1034,7 @@ async def test_http_client_timeout_option(self) -> None: # no timeout given to the httpx client should not use the httpx default async with httpx.AsyncClient() as http_client: - client = AsyncSteel( - base_url=base_url, - steel_api_key=steel_api_key, - _strict_response_validation=True, - http_client=http_client, - ) + client = AsyncSteel(base_url=base_url, _strict_response_validation=True, http_client=http_client) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore @@ -1138,12 +1042,7 @@ async def test_http_client_timeout_option(self) -> None: # explicitly passing the default timeout currently results in it being ignored async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: - client = AsyncSteel( - base_url=base_url, - steel_api_key=steel_api_key, - _strict_response_validation=True, - http_client=http_client, - ) + client = AsyncSteel(base_url=base_url, _strict_response_validation=True, http_client=http_client) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore @@ -1152,27 +1051,16 @@ async def test_http_client_timeout_option(self) -> None: def test_invalid_http_client(self) -> None: with pytest.raises(TypeError, match="Invalid `http_client` arg"): with httpx.Client() as http_client: - AsyncSteel( - base_url=base_url, - steel_api_key=steel_api_key, - _strict_response_validation=True, - http_client=cast(Any, http_client), - ) + AsyncSteel(base_url=base_url, _strict_response_validation=True, http_client=cast(Any, http_client)) def test_default_headers_option(self) -> None: - client = AsyncSteel( - base_url=base_url, - steel_api_key=steel_api_key, - _strict_response_validation=True, - default_headers={"X-Foo": "bar"}, - ) + client = AsyncSteel(base_url=base_url, _strict_response_validation=True, default_headers={"X-Foo": "bar"}) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "bar" assert request.headers.get("x-stainless-lang") == "python" client2 = AsyncSteel( base_url=base_url, - steel_api_key=steel_api_key, _strict_response_validation=True, default_headers={ "X-Foo": "stainless", @@ -1184,12 +1072,7 @@ def test_default_headers_option(self) -> None: assert request.headers.get("x-stainless-lang") == "my-overriding-header" def test_default_query_option(self) -> None: - client = AsyncSteel( - base_url=base_url, - steel_api_key=steel_api_key, - _strict_response_validation=True, - default_query={"query_param": "bar"}, - ) + client = AsyncSteel(base_url=base_url, _strict_response_validation=True, default_query={"query_param": "bar"}) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) url = httpx.URL(request.url) assert dict(url.params) == {"query_param": "bar"} @@ -1388,9 +1271,7 @@ class Model(BaseModel): assert response.foo == 2 def test_base_url_setter(self) -> None: - client = AsyncSteel( - base_url="https://example.com/from_init", steel_api_key=steel_api_key, _strict_response_validation=True - ) + client = AsyncSteel(base_url="https://example.com/from_init", _strict_response_validation=True) assert client.base_url == "https://example.com/from_init/" client.base_url = "https://example.com/from_setter" # type: ignore[assignment] @@ -1399,20 +1280,15 @@ def test_base_url_setter(self) -> None: def test_base_url_env(self) -> None: with update_env(STEEL_BASE_URL="http://localhost:5000/from/env"): - client = AsyncSteel(steel_api_key=steel_api_key, _strict_response_validation=True) + client = AsyncSteel(_strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" @pytest.mark.parametrize( "client", [ + AsyncSteel(base_url="http://localhost:5000/custom/path/", _strict_response_validation=True), AsyncSteel( base_url="http://localhost:5000/custom/path/", - steel_api_key=steel_api_key, - _strict_response_validation=True, - ), - AsyncSteel( - base_url="http://localhost:5000/custom/path/", - steel_api_key=steel_api_key, _strict_response_validation=True, http_client=httpx.AsyncClient(), ), @@ -1432,14 +1308,9 @@ def test_base_url_trailing_slash(self, client: AsyncSteel) -> None: @pytest.mark.parametrize( "client", [ + AsyncSteel(base_url="http://localhost:5000/custom/path/", _strict_response_validation=True), AsyncSteel( base_url="http://localhost:5000/custom/path/", - steel_api_key=steel_api_key, - _strict_response_validation=True, - ), - AsyncSteel( - base_url="http://localhost:5000/custom/path/", - steel_api_key=steel_api_key, _strict_response_validation=True, http_client=httpx.AsyncClient(), ), @@ -1459,14 +1330,9 @@ def test_base_url_no_trailing_slash(self, client: AsyncSteel) -> None: @pytest.mark.parametrize( "client", [ + AsyncSteel(base_url="http://localhost:5000/custom/path/", _strict_response_validation=True), AsyncSteel( base_url="http://localhost:5000/custom/path/", - steel_api_key=steel_api_key, - _strict_response_validation=True, - ), - AsyncSteel( - base_url="http://localhost:5000/custom/path/", - steel_api_key=steel_api_key, _strict_response_validation=True, http_client=httpx.AsyncClient(), ), @@ -1484,7 +1350,7 @@ def test_absolute_request_url(self, client: AsyncSteel) -> None: assert request.url == "https://myapi.com/foo" async def test_copied_client_does_not_close_http(self) -> None: - client = AsyncSteel(base_url=base_url, steel_api_key=steel_api_key, _strict_response_validation=True) + client = AsyncSteel(base_url=base_url, _strict_response_validation=True) assert not client.is_closed() copied = client.copy() @@ -1496,7 +1362,7 @@ async def test_copied_client_does_not_close_http(self) -> None: assert not client.is_closed() async def test_client_context_manager(self) -> None: - client = AsyncSteel(base_url=base_url, steel_api_key=steel_api_key, _strict_response_validation=True) + client = AsyncSteel(base_url=base_url, _strict_response_validation=True) async with client as c2: assert c2 is client assert not c2.is_closed() @@ -1518,12 +1384,7 @@ class Model(BaseModel): async def test_client_max_retries_validation(self) -> None: with pytest.raises(TypeError, match=r"max_retries cannot be None"): - AsyncSteel( - base_url=base_url, - steel_api_key=steel_api_key, - _strict_response_validation=True, - max_retries=cast(Any, None), - ) + AsyncSteel(base_url=base_url, _strict_response_validation=True, max_retries=cast(Any, None)) @pytest.mark.respx(base_url=base_url) @pytest.mark.asyncio @@ -1533,12 +1394,12 @@ class Model(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) - strict_client = AsyncSteel(base_url=base_url, steel_api_key=steel_api_key, _strict_response_validation=True) + strict_client = AsyncSteel(base_url=base_url, _strict_response_validation=True) with pytest.raises(APIResponseValidationError): await strict_client.get("/foo", cast_to=Model) - client = AsyncSteel(base_url=base_url, steel_api_key=steel_api_key, _strict_response_validation=False) + client = AsyncSteel(base_url=base_url, _strict_response_validation=False) response = await client.get("/foo", cast_to=Model) assert isinstance(response, str) # type: ignore[unreachable] @@ -1567,7 +1428,7 @@ class Model(BaseModel): @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) @pytest.mark.asyncio async def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None: - client = AsyncSteel(base_url=base_url, steel_api_key=steel_api_key, _strict_response_validation=True) + client = AsyncSteel(base_url=base_url, _strict_response_validation=True) headers = httpx.Headers({"retry-after": retry_after}) options = FinalRequestOptions(method="get", url="/foo", max_retries=3) From 0500644bda4e13e3ff0975a3be3af641fb731dea Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 27 Jun 2025 02:41:45 +0000 Subject: [PATCH 13/16] =?UTF-8?q?fix(ci):=20release-doctor=20=E2=80=94=20r?= =?UTF-8?q?eport=20correct=20token=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/check-release-environment | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/check-release-environment b/bin/check-release-environment index ae0cc4a5..b845b0f4 100644 --- a/bin/check-release-environment +++ b/bin/check-release-environment @@ -3,7 +3,7 @@ errors=() if [ -z "${PYPI_TOKEN}" ]; then - errors+=("The STEEL_PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") + errors+=("The PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") fi lenErrors=${#errors[@]} From 51c348d7d8d1811dbd054887b06a3a0fc582d61e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 28 Jun 2025 08:51:03 +0000 Subject: [PATCH 14/16] chore(ci): only run for pushes and fork pull requests --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52281f3e..4bf2be61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,7 @@ jobs: timeout-minutes: 10 name: lint runs-on: ${{ github.repository == 'stainless-sdks/steel-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@v4 @@ -42,6 +43,7 @@ jobs: contents: read id-token: write runs-on: depot-ubuntu-24.04 + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@v4 @@ -62,6 +64,7 @@ jobs: timeout-minutes: 10 name: test runs-on: ${{ github.repository == 'stainless-sdks/steel-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@v4 From 76d9c3f76a200bc1d2e567bd0bf9decebf6998cc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 02:36:45 +0000 Subject: [PATCH 15/16] fix(ci): correct conditional --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4bf2be61..c002b809 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,14 +36,13 @@ jobs: run: ./scripts/lint upload: - if: github.repository == 'stainless-sdks/steel-python' + if: github.repository == 'stainless-sdks/steel-python' && (github.event_name == 'push' || github.event.pull_request.head.repo.fork) timeout-minutes: 10 name: upload permissions: contents: read id-token: write runs-on: depot-ubuntu-24.04 - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@v4 From a9a81fe0e5eb6740b69ea37ae538a6d75cc7018d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 02:37:21 +0000 Subject: [PATCH 16/16] release: 0.5.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- src/steel/_version.py | 2 +- 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index da59f99e..2aca35ae 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.4.0" + ".": "0.5.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a3e88cbb..08a0b09d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,38 @@ # Changelog +## 0.5.0 (2025-06-30) + +Full Changelog: [v0.4.0...v0.5.0](https://github.com/steel-dev/steel-python/compare/v0.4.0...v0.5.0) + +### Features + +* **api:** api update ([978ad9c](https://github.com/steel-dev/steel-python/commit/978ad9cb86523cf783ca1c059b137d497422d626)) +* **client:** add support for aiohttp ([87bc6e2](https://github.com/steel-dev/steel-python/commit/87bc6e2420ea32f8336251fdda4c8353b8181f35)) + + +### Bug Fixes + +* **ci:** correct conditional ([76d9c3f](https://github.com/steel-dev/steel-python/commit/76d9c3f76a200bc1d2e567bd0bf9decebf6998cc)) +* **ci:** release-doctor — report correct token name ([0500644](https://github.com/steel-dev/steel-python/commit/0500644bda4e13e3ff0975a3be3af641fb731dea)) +* **client:** correctly parse binary response | stream ([96bc0a5](https://github.com/steel-dev/steel-python/commit/96bc0a57989271ac57a5e916a9678798e8bc7123)) +* **tests:** fix: tests which call HTTP endpoints directly with the example parameters ([ffacf10](https://github.com/steel-dev/steel-python/commit/ffacf10a70b267665f65ee31aa176a2d0fdb0fca)) + + +### Chores + +* **ci:** enable for pull requests ([4f581f1](https://github.com/steel-dev/steel-python/commit/4f581f14fac853d9ffb09aac7efcd4cba8e7e008)) +* **ci:** only run for pushes and fork pull requests ([51c348d](https://github.com/steel-dev/steel-python/commit/51c348d7d8d1811dbd054887b06a3a0fc582d61e)) +* **internal:** update conftest.py ([95540f2](https://github.com/steel-dev/steel-python/commit/95540f26791852e6a9864e4ebe12fb8762ed0241)) +* **readme:** update badges ([e210ee7](https://github.com/steel-dev/steel-python/commit/e210ee78b18dc19b9b39b67c2b37f16c855f0c58)) +* **tests:** add tests for httpx client instantiation & proxies ([ad4f8a1](https://github.com/steel-dev/steel-python/commit/ad4f8a1256746d265dd9a2c0a9bfca53fa4c61f3)) +* **tests:** run tests in parallel ([825c97f](https://github.com/steel-dev/steel-python/commit/825c97fc4b37a582267bf338b2640de17a254546)) +* **tests:** skip some failing tests on the latest python versions ([c64047c](https://github.com/steel-dev/steel-python/commit/c64047c3036b3054f1f466f7521db5f76d013c8d)) + + +### Documentation + +* **client:** fix httpx.Timeout documentation reference ([2c5ba6f](https://github.com/steel-dev/steel-python/commit/2c5ba6f2f450b6b7a2172ee9dfcf9de93426773b)) + ## 0.4.0 (2025-06-03) Full Changelog: [v0.3.0...v0.4.0](https://github.com/steel-dev/steel-python/compare/v0.3.0...v0.4.0) diff --git a/pyproject.toml b/pyproject.toml index 7e594f3b..a7be57df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "steel-sdk" -version = "0.4.0" +version = "0.5.0" description = "The official Python library for the steel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/steel/_version.py b/src/steel/_version.py index e1ac008d..62dd1dd2 100644 --- a/src/steel/_version.py +++ b/src/steel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "steel" -__version__ = "0.4.0" # x-release-please-version +__version__ = "0.5.0" # x-release-please-version