From 3b19cccd5cf2fe2291adb705d3f55907b2bd8307 Mon Sep 17 00:00:00 2001 From: Richard Marmorstein Date: Fri, 23 Feb 2024 18:19:58 -0800 Subject: [PATCH 1/7] wip --- stripe/__init__.py | 1 + stripe/_http_client.py | 122 ++++++++++- test-requirements.txt | 1 + tests/test_http_client.py | 417 +++++++++++++++++++++++++++++++++++++- tests/test_integration.py | 68 +++++-- 5 files changed, 586 insertions(+), 23 deletions(-) diff --git a/stripe/__init__.py b/stripe/__init__.py index 222a08e32..911c03a90 100644 --- a/stripe/__init__.py +++ b/stripe/__init__.py @@ -165,6 +165,7 @@ def set_app_info( RequestsClient as RequestsClient, UrlFetchClient as UrlFetchClient, HTTPXClient as HTTPXClient, + AIOHTTPClient as AIOHTTPClient, new_default_http_client as new_default_http_client, ) diff --git a/stripe/_http_client.py b/stripe/_http_client.py index 1337103bc..42b8fed0e 100644 --- a/stripe/_http_client.py +++ b/stripe/_http_client.py @@ -6,8 +6,12 @@ import random import threading import json +import asyncio +import ssl from http.client import HTTPResponse +from aiohttp import TCPConnector + # Used for global variables import stripe # noqa: IMP101 from stripe import _util @@ -61,6 +65,13 @@ httpx = None anyio = None +try: + import aiohttp + from aiohttp import ClientTimeout as AIOHTTPTimeout + from aiohttp import StreamReader as AIOHTTPStreamReader +except ImportError: + aiohttp = None + try: import requests from requests import Session as RequestsSession @@ -121,6 +132,8 @@ def new_default_http_client(*args: Any, **kwargs: Any) -> "HTTPClient": def new_http_client_async_fallback(*args: Any, **kwargs: Any) -> "HTTPClient": if httpx: impl = HTTPXClient + elif aiohttp: + impl = AIOHTTPClient else: impl = NoImportFoundAsyncClient @@ -1261,7 +1274,6 @@ def request( url: str, headers: Mapping[str, str], post_data=None, - timeout: float = 80.0, ) -> Tuple[bytes, int, Mapping[str, str]]: args, kwargs = self._get_request_args_kwargs( method, url, headers, post_data @@ -1282,7 +1294,6 @@ async def request_async( url: str, headers: Mapping[str, str], post_data=None, - timeout: float = 80.0, ) -> Tuple[bytes, int, Mapping[str, str]]: args, kwargs = self._get_request_args_kwargs( method, url, headers, post_data @@ -1353,6 +1364,113 @@ async def close_async(self): await self._client_async.aclose() +class AIOHTTPClient(HTTPClient): + name = "aiohttp" + + def __init__( + self, timeout: Optional[Union[float, "AIOHTTPTimeout"]] = 80, **kwargs + ): + super(AIOHTTPClient, self).__init__(**kwargs) + + if aiohttp is None: + raise ImportError( + "Unexpected: tried to initialize AIOHTTPClient but the aiohttp module is not present." + ) + + self.httpx = httpx + + kwargs = {} + if self._verify_ssl_certs: + ssl_context = ssl.create_default_context( + capath=stripe.ca_bundle_path + ) + kwargs["connector"] = TCPConnector(ssl=ssl_context) + + self._session = aiohttp.ClientSession(**kwargs) + self._timeout = timeout + + def sleep_async(self, secs): + return asyncio.sleep(secs) + + def request(self) -> Tuple[bytes, int, Mapping[str, str]]: + raise NotImplementedError( + "AIOHTTPClient does not support synchronous requests." + ) + + def _get_request_args_kwargs( + self, method: str, url: str, headers: Mapping[str, str], post_data + ): + args = (method, url) + kwargs = {} + if self._proxy: + if self._proxy["http"] != self._proxy["https"]: + raise ValueError( + "AIOHTTPClient does not support different proxies for HTTP and HTTPS." + ) + kwargs["proxy"] = self._proxy["https"] + if self._timeout: + kwargs["timeout"] = self._timeout + + kwargs["headers"] = headers + kwargs["data"] = post_data + return args, kwargs + + async def request_async( + self, + method: str, + url: str, + headers: Mapping[str, str], + post_data=None, + ) -> Tuple[bytes, int, Mapping[str, str]]: + ( + content, + status_code, + response_headers, + ) = await self.request_stream_async( + method, url, headers, post_data=post_data + ) + + return (await content.read()), status_code, response_headers + + def _handle_request_error(self, e) -> NoReturn: + msg = ( + "Unexpected error communicating with Stripe. If this " + "problem persists, let us know at support@stripe.com." + ) + err = "A %s was raised" % (type(e).__name__,) + should_retry = True + + msg = textwrap.fill(msg) + "\n\n(Network error: %s)" % (err,) + raise APIConnectionError(msg, should_retry=should_retry) + + def request_stream(self) -> Tuple[Iterable[bytes], int, Mapping[str, str]]: + raise NotImplementedError( + "AIOHTTPClient does not support synchronous requests." + ) + + async def request_stream_async( + self, method: str, url: str, headers: Mapping[str, str], post_data=None + ) -> Tuple["AIOHTTPStreamReader", int, Mapping[str, str]]: + args, kwargs = self._get_request_args_kwargs( + method, url, headers, post_data + ) + try: + response = await self._session.request(*args, **kwargs) + except Exception as e: + self._handle_request_error(e) + + content = response.content + status_code = response.status + response_headers = response.headers + return content, status_code, response_headers + + def close(self): + pass + + async def close_async(self): + await self._session.close() + + class NoImportFoundAsyncClient(HTTPClient): def __init__(self, **kwargs): super(NoImportFoundAsyncClient, self).__init__(**kwargs) diff --git a/test-requirements.txt b/test-requirements.txt index 450a44767..875b63734 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,6 +2,7 @@ # This is the last version of httpx compatible with Python 3.6 httpx == 0.22.0 +aiohttp == 3.8.6 anyio[trio] == 3.6.2 pytest-cov >= 2.8.1, < 2.11.0 diff --git a/tests/test_http_client.py b/tests/test_http_client.py index ffe85fed5..246539a2e 100644 --- a/tests/test_http_client.py +++ b/tests/test_http_client.py @@ -22,6 +22,7 @@ class StripeClientTestCase(object): ("pycurl", "stripe._http_client.pycurl"), ("urllib.request", "stripe._http_client.urllibrequest"), ("httpx", "stripe._http_client.httpx"), + ("aiohttp", "stripe._http_client.aiohttp"), ] @pytest.fixture @@ -69,11 +70,20 @@ def check_default(self, none_libs, expected): def test_new_http_client_async_fallback_httpx(self, request_mocks): self.check_default((), _http_client.HTTPXClient) + def test_new_http_client_async_fallback_aiohttp(self, request_mocks): + self.check_default( + (("httpx"),), + _http_client.AIOHTTPClient, + ) + def test_new_http_client_async_fallback_no_import_found( self, request_mocks ): self.check_default( - (("httpx"),), + ( + ("httpx"), + ("aiohttp"), + ), _http_client.NoImportFoundAsyncClient, ) @@ -1505,3 +1515,408 @@ def test_timeout(self): def test_timeout_async(self): pass + + +class TestAIOHTTPClient(StripeClientTestCase, ClientTestBase): + REQUEST_CLIENT: Type[ + _http_client.AIOHTTPClient + ] = _http_client.AIOHTTPClient + + @pytest.fixture + def mock_response(self, mocker, request_mock): + def mock_response(mock, body={}, code=200): + result = mocker.Mock() + result.content = mocker.MagicMock() + + result.content.__aiter__.return_value = [ + bytes(body, "utf-8") if isinstance(body, str) else body + ] + result.status = code + result.headers = {} + result.content.read = mocker.AsyncMock(return_value=body) + + request_mock.ClientSession().request = mocker.AsyncMock( + return_value=result + ) + return result + + return mock_response + + @pytest.fixture + def mock_error(self, mocker, request_mock): + def mock_error(mock): + # The first kind of request exceptions we catch + mock.exceptions.SSLError = Exception + request_mock.ClientSession().request.side_effect = ( + mock.exceptions.SSLError() + ) + + return mock_error + + @pytest.fixture + def check_call(self, request_mock, mocker): + def check_call( + mock, + method, + url, + post_data, + headers, + is_streaming=False, + timeout=80, + times=None, + ): + times = times or 1 + args = (method, url) + kwargs = { + "headers": headers, + "data": post_data or {}, + "timeout": timeout, + "proxies": {"http": "http://slap/", "https": "http://slap/"}, + } + + if is_streaming: + kwargs["stream"] = True + + calls = [mocker.call(*args, **kwargs) for _ in range(times)] + request_mock.ClientSession().request.assert_has_calls(calls) + + return check_call + + @pytest.fixture + def check_call_async(self, request_mock, mocker): + def check_call_async( + mock, + method, + url, + post_data, + headers, + is_streaming=False, + timeout=80, + times=None, + ): + times = times or 1 + args = (method, url) + kwargs = { + "headers": headers, + "data": post_data, + "timeout": timeout, + "proxy": "http://slap/", + } + + calls = [mocker.call(*args, **kwargs) for _ in range(times)] + request_mock.ClientSession().request.assert_has_calls(calls) + + return check_call_async + + def make_request(self, method, url, headers, post_data, timeout=80): + pass + + async def make_request_async( + self, method, url, headers, post_data, timeout=80 + ): + client = self.REQUEST_CLIENT( + verify_ssl_certs=True, proxy="http://slap/", timeout=timeout + ) + return await client.request_with_retries_async( + method, url, headers, post_data + ) + + async def make_request_stream_async( + self, method, url, headers, post_data, timeout=80 + ): + client = self.REQUEST_CLIENT( + verify_ssl_certs=True, proxy="http://slap/" + ) + return await client.request_stream_with_retries_async( + method, url, headers, post_data + ) + + def test_request(self): + pass + + def test_request_stream(self): + pass + + @pytest.mark.parametrize("anyio_backend", ["asyncio"]) + @pytest.mark.anyio + async def test_request_async( + self, request_mock, mock_response, check_call_async + ): + + mock_response(request_mock, '{"foo": "baz"}', 200) + + for method in VALID_API_METHODS: + abs_url = self.valid_url + data = {} + + if method != "post": + abs_url = "%s?%s" % (abs_url, data) + data = {} + + headers = {"my-header": "header val"} + body, code, _ = await self.make_request_async( + method, abs_url, headers, data + ) + assert code == 200 + assert body == '{"foo": "baz"}' + + check_call_async(request_mock, method, abs_url, data, headers) + + @pytest.mark.parametrize("anyio_backend", ["asyncio"]) + @pytest.mark.anyio + async def test_request_stream_async( + self, mocker, request_mock, mock_response, check_call + ): + for method in VALID_API_METHODS: + mock_response(request_mock, "some streamed content", 200) + + abs_url = self.valid_url + data = "" + + if method != "post": + abs_url = "%s?%s" % (abs_url, data) + data = None + + headers = {"my-header": "header val"} + + stream, code, _ = await self.make_request_stream_async( + method, abs_url, headers, data + ) + + assert code == 200 + + # Here we need to convert and align all content on one type (string) + # as some clients return a string stream others a byte stream. + body_content = b"".join([x async for x in stream]) + if hasattr(body_content, "decode"): + body_content = body_content.decode("utf-8") + + assert body_content == "some streamed content" + + mocker.resetall() + + @pytest.mark.parametrize("anyio_backend", ["asyncio"]) + @pytest.mark.anyio + async def test_exception(self, request_mock, mock_error): + mock_error(request_mock) + with pytest.raises(stripe.APIConnectionError): + await self.make_request_async("get", self.valid_url, {}, None) + + def test_timeout( + self, request_mock, mock_response, check_call, anyio_backend + ): + pass + + @pytest.mark.parametrize("anyio_backend", ["asyncio"]) + @pytest.mark.anyio + async def test_timeout_async( + self, request_mock, mock_response, check_call_async + ): + headers = {"my-header": "header val"} + data = {} + mock_response(request_mock, '{"foo": "baz"}', 200) + await self.make_request_async( + "POST", self.valid_url, headers, data, timeout=5 + ) + + check_call_async( + request_mock, "POST", self.valid_url, data, headers, timeout=5 + ) + + @pytest.mark.parametrize("anyio_backend", ["asyncio"]) + @pytest.mark.anyio + async def test_request_stream_forwards_stream_param( + self, mocker, request_mock, mock_response, check_call + ): + # TODO + pass + + +class TestAIOHTTPClientRetryBehavior(TestAIOHTTPClient): + responses = None + + @pytest.fixture + def mock_retry(self, mocker, request_mock): + def mock_retry( + retry_error_num=0, no_retry_error_num=0, responses=None + ): + if responses is None: + responses = [] + # Mocking classes of exception we catch. Any group of exceptions + # with the same inheritance pattern will work + request_root_error_class = Exception + request_mock.exceptions.RequestException = request_root_error_class + + no_retry_parent_class = LookupError + no_retry_child_class = KeyError + request_mock.exceptions.SSLError = no_retry_parent_class + no_retry_errors = [no_retry_child_class()] * no_retry_error_num + + retry_parent_class = EnvironmentError + retry_child_class = IOError + request_mock.exceptions.Timeout = retry_parent_class + request_mock.exceptions.ConnectionError = retry_parent_class + retry_errors = [retry_child_class()] * retry_error_num + # Include mock responses as possible side-effects + # to simulate returning proper results after some exceptions + + results = retry_errors + no_retry_errors + responses + + request_mock.ClientSession().request = AsyncMock( + side_effect=results + ) + self.responses = results + + return request_mock + + return mock_retry + + @pytest.fixture + def check_call_numbers(self, check_call_async): + valid_url = self.valid_url + + def check_call_numbers(times, is_streaming=False): + check_call_async( + None, + "GET", + valid_url, + None, + {}, + times=times, + is_streaming=is_streaming, + ) + + return check_call_numbers + + def max_retries(self): + return 3 + + def make_client(self): + client = self.REQUEST_CLIENT( + verify_ssl_certs=True, timeout=80, proxy="http://slap/" + ) + # Override sleep time to speed up tests + client._sleep_time_seconds = lambda num_retries, response=None: 0.0001 + # Override configured max retries + return client + + def make_request(self, *args, **kwargs): + pass + + def make_request_stream(self, *args, **kwargs): + pass + + async def make_request_async(self, *args, **kwargs): + client = self.make_client() + return await client.request_with_retries_async( + "GET", self.valid_url, {}, None, self.max_retries() + ) + + async def make_request_stream_async(self, *args, **kwargs): + client = self.make_client() + return await client.request_stream_with_retries_async( + "GET", self.valid_url, {}, None, self.max_retries() + ) + + @pytest.mark.parametrize("anyio_backend", ["asyncio"]) + @pytest.mark.anyio + async def test_retry_error_until_response( + self, + mock_retry, + mock_response, + check_call_numbers, + request_mock, + mocker, + ): + mock_retry( + retry_error_num=1, + responses=[mock_response(request_mock, code=202)], + ) + _, code, _ = await self.make_request_async() + assert code == 202 + check_call_numbers(2) + + @pytest.mark.parametrize("anyio_backend", ["asyncio"]) + @pytest.mark.anyio + async def test_retry_error_until_exceeded( + self, mock_retry, mock_response, check_call_numbers + ): + mock_retry(retry_error_num=self.max_retries()) + with pytest.raises(stripe.APIConnectionError): + await self.make_request_async() + + check_call_numbers(self.max_retries()) + + @pytest.mark.parametrize("anyio_backend", ["asyncio"]) + @pytest.mark.anyio + async def test_no_retry_error( + self, mock_retry, mock_response, check_call_numbers + ): + mock_retry(no_retry_error_num=self.max_retries()) + with pytest.raises(stripe.APIConnectionError): + await self.make_request_async() + check_call_numbers(1) + + @pytest.mark.parametrize("anyio_backend", ["asyncio"]) + @pytest.mark.anyio + async def test_retry_codes( + self, mock_retry, mock_response, request_mock, check_call_numbers + ): + mock_retry( + responses=[ + mock_response(request_mock, code=409), + mock_response(request_mock, code=202), + ] + ) + _, code, _ = await self.make_request_async() + assert code == 202 + check_call_numbers(2) + + @pytest.mark.parametrize("anyio_backend", ["asyncio"]) + @pytest.mark.anyio + async def test_retry_codes_until_exceeded( + self, mock_retry, mock_response, request_mock, check_call_numbers + ): + mock_retry( + responses=[mock_response(request_mock, code=409)] + * (self.max_retries() + 1) + ) + _, code, _ = await self.make_request_async() + assert code == 409 + check_call_numbers(self.max_retries() + 1) + + def connection_error(self, client, given_exception): + with pytest.raises(stripe.APIConnectionError) as error: + client._handle_request_error(given_exception) + return error.value + + @pytest.mark.parametrize("anyio_backend", ["asyncio"]) + @pytest.mark.anyio + async def test_handle_request_error_should_retry( + self, mock_retry, anyio_backend + ): + client = self.REQUEST_CLIENT() + request_mock = mock_retry() + + error = self.connection_error( + client, request_mock.exceptions.Timeout() + ) + assert error.should_retry + + error = self.connection_error( + client, request_mock.exceptions.ConnectionError() + ) + assert error.should_retry + + # Skip inherited basic client tests + def test_request(self): + pass + + def test_request_async(self): + pass + + def test_timeout(self): + pass + + def test_timeout_async(self): + pass diff --git a/tests/test_integration.py b/tests/test_integration.py index e952dbc43..01bc256f1 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,10 +1,12 @@ import platform from threading import Thread, Lock import json +from typing import cast, Any import warnings import time import httpx +import aiohttp import stripe import pytest @@ -72,6 +74,8 @@ def do_request( class TestIntegration(object): + mock_server = None + @pytest.fixture(autouse=True) def close_mock_server(self): yield @@ -103,6 +107,7 @@ def setup_stripe(self): stripe.api_base = orig_attrs["api_base"] stripe.upload_api_base = orig_attrs["api_base"] stripe.api_key = orig_attrs["api_key"] + stripe.default_http_client = orig_attrs["default_http_client"] stripe.enable_telemetry = orig_attrs["enable_telemetry"] stripe.max_network_retries = orig_attrs["max_network_retries"] stripe.proxy = orig_attrs["proxy"] @@ -328,7 +333,24 @@ def do_request(self, req_num): assert usage == ["stripe_client"] @pytest.mark.anyio - async def test_async_raw_request_success(self): + @pytest.fixture(params=["aiohttp", "httpx"]) + async def async_http_client(self, request, anyio_backend): + if request.param == "httpx": + return stripe.HTTPXClient() + elif request.param == "aiohttp": + if anyio_backend != "asyncio": + return pytest.skip("aiohttp only works with asyncio") + return stripe.AIOHTTPClient() + else: + raise ValueError(f"Unknown http client name: {request.param}") + + @pytest.fixture + async def set_global_async_http_client(self, async_http_client): + stripe.default_http_client = async_http_client + + async def test_async_raw_request_success( + self, set_global_async_http_client + ): class MockServerRequestHandler(MyTestHandler): default_body = '{"id": "cus_123", "object": "customer"}'.encode( "utf-8" @@ -351,8 +373,9 @@ class MockServerRequestHandler(MyTestHandler): assert req.command == "POST" assert isinstance(cus, stripe.Customer) - @pytest.mark.anyio - async def test_async_raw_request_timeout(self): + async def test_async_raw_request_timeout( + self, set_global_async_http_client + ): class MockServerRequestHandler(MyTestHandler): def do_request(self, n): time.sleep(0.02) @@ -360,16 +383,19 @@ def do_request(self, n): self.setup_mock_server(MockServerRequestHandler) stripe.api_base = "http://localhost:%s" % self.mock_server_port - stripe.default_http_client = stripe.new_default_http_client() - assert isinstance(stripe.default_http_client, stripe.RequestsClient) - stripe.default_http_client._async_fallback_client = ( - stripe.HTTPXClient() - ) # If we set HTTPX's generic timeout the test is flaky (sometimes it's a ReadTimeout, sometimes its a ConnectTimeout) # so we set only the read timeout specifically. - stripe.default_http_client._async_fallback_client._timeout = ( - httpx.Timeout(None, read=0.01) - ) + hc = stripe.default_http_client + + expected_message = "" + if isinstance(hc, stripe.HTTPXClient): + hc._timeout = httpx.Timeout(None, read=0.01) + expected_message = "A ReadTimeout was raised" + elif isinstance(hc, stripe.AIOHTTPClient): + hc._timeout = aiohttp.ClientTimeout(sock_read=0.01) + expected_message = "A ServerTimeoutError was raised" + else: + raise ValueError(f"Unknown http client: {hc.name}") stripe.max_network_retries = 0 exception = None @@ -382,10 +408,11 @@ def do_request(self, n): assert exception is not None - assert "A ReadTimeout was raised" in str(exception.user_message) + assert expected_message in str(exception.user_message) - @pytest.mark.anyio - async def test_async_httpx_raw_request_retries(self): + async def test_async_raw_request_retries( + self, set_global_async_http_client + ): class MockServerRequestHandler(MyTestHandler): def do_request(self, n): if n == 0: @@ -410,8 +437,9 @@ def do_request(self, n): assert req.path == "/v1/customers" - @pytest.mark.anyio - async def test_async_httpx_raw_request_unretryable(self): + async def test_async_raw_request_unretryable( + self, set_global_async_http_client + ): class MockServerRequestHandler(MyTestHandler): def do_request(self, n): return ( @@ -437,8 +465,7 @@ def do_request(self, n): assert exception is not None assert "Unauthorized" in str(exception.user_message) - @pytest.mark.anyio - async def test_async_httpx_stream(self): + async def test_async_httpx_stream(self, set_global_async_http_client): class MockServerRequestHandler(MyTestHandler): def do_request(self, n): return (200, None, b"hello") @@ -449,8 +476,9 @@ def do_request(self, n): result = await stripe.Quote.pdf_async("qt_123") assert str(await result.read(), "utf-8") == "hello" - @pytest.mark.anyio - async def test_async_httpx_stream_error(self): + async def test_async_httpx_stream_error( + self, set_global_async_http_client + ): class MockServerRequestHandler(MyTestHandler): def do_request(self, n): return (400, None, b'{"error": {"message": "bad request"}}') From af1372f2b85a95951130ac6cac391c6a232b8d2e Mon Sep 17 00:00:00 2001 From: Richard Marmorstein Date: Mon, 26 Feb 2024 10:07:54 -0800 Subject: [PATCH 2/7] Fix lint --- tests/test_integration.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_integration.py b/tests/test_integration.py index 01bc256f1..34ae23dd8 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,7 +1,6 @@ import platform from threading import Thread, Lock import json -from typing import cast, Any import warnings import time From 2080e6ab4e9fb36f279d01469f40ccefe191e2c7 Mon Sep 17 00:00:00 2001 From: Richard Marmorstein Date: Mon, 26 Feb 2024 10:16:05 -0800 Subject: [PATCH 3/7] Fix test --- tests/test_http_client.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/test_http_client.py b/tests/test_http_client.py index 246539a2e..0d5aa9057 100644 --- a/tests/test_http_client.py +++ b/tests/test_http_client.py @@ -70,7 +70,16 @@ def check_default(self, none_libs, expected): def test_new_http_client_async_fallback_httpx(self, request_mocks): self.check_default((), _http_client.HTTPXClient) - def test_new_http_client_async_fallback_aiohttp(self, request_mocks): + # Using the AIOHTTPClient constructor will complain if there's + # no active asyncio event loop. This test can pass in isolation + # but if it runs after another asyncio-enabled test that closes + # the asyncio event loop it will fail unless it is declared to + # use the asyncio backend. + @pytest.mark.anyio + @pytest.mark.parametrize("anyio_backend", ["asyncio"]) + async def test_new_http_client_async_fallback_aiohttp( + self, request_mocks, anyio_backend + ): self.check_default( (("httpx"),), _http_client.AIOHTTPClient, From 63b0af2f96223e3f63d04d904336034e0cb088e7 Mon Sep 17 00:00:00 2001 From: Richard Marmorstein Date: Mon, 26 Feb 2024 11:30:55 -0800 Subject: [PATCH 4/7] Get tests to pass in older python versions --- tests/test_http_client.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/tests/test_http_client.py b/tests/test_http_client.py index 0d5aa9057..1f250325e 100644 --- a/tests/test_http_client.py +++ b/tests/test_http_client.py @@ -1534,17 +1534,25 @@ class TestAIOHTTPClient(StripeClientTestCase, ClientTestBase): @pytest.fixture def mock_response(self, mocker, request_mock): def mock_response(mock, body={}, code=200): - result = mocker.Mock() - result.content = mocker.MagicMock() - result.content.__aiter__.return_value = [ - bytes(body, "utf-8") if isinstance(body, str) else body - ] - result.status = code - result.headers = {} - result.content.read = mocker.AsyncMock(return_value=body) + class Content: + def __aiter__(self): + async def chunk(): + yield bytes(body, "utf-8") if isinstance(body, str) else body + return chunk() + + async def read(self): + return body - request_mock.ClientSession().request = mocker.AsyncMock( + class Result: + def __init__(self): + self.content = Content() + self.status = code + self.headers = {} + + result = Result() + + request_mock.ClientSession().request = AsyncMock( return_value=result ) return result From de3e52777229f16914a412c36e1ae12f5d2b93f3 Mon Sep 17 00:00:00 2001 From: Richard Marmorstein Date: Mon, 26 Feb 2024 16:06:20 -0800 Subject: [PATCH 5/7] rejigger aiohttp versions --- test-requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 875b63734..d0bf84972 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,7 +2,8 @@ # This is the last version of httpx compatible with Python 3.6 httpx == 0.22.0 -aiohttp == 3.8.6 +aiohttp == 3.8.6; python_version <= "3.7" +aiohttp == 3.9.0; python_version > "3.7" anyio[trio] == 3.6.2 pytest-cov >= 2.8.1, < 2.11.0 From 7e424c889bff3f8e60f0f72e4459b3e4990af015 Mon Sep 17 00:00:00 2001 From: Richard Marmorstein Date: Mon, 26 Feb 2024 16:10:54 -0800 Subject: [PATCH 6/7] Format --- tests/test_http_client.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_http_client.py b/tests/test_http_client.py index 1f250325e..f16a40173 100644 --- a/tests/test_http_client.py +++ b/tests/test_http_client.py @@ -1534,11 +1534,13 @@ class TestAIOHTTPClient(StripeClientTestCase, ClientTestBase): @pytest.fixture def mock_response(self, mocker, request_mock): def mock_response(mock, body={}, code=200): - class Content: def __aiter__(self): async def chunk(): - yield bytes(body, "utf-8") if isinstance(body, str) else body + yield bytes(body, "utf-8") if isinstance( + body, str + ) else body + return chunk() async def read(self): From 385f1f197ee4acf65bd33a868f444506294ad1b4 Mon Sep 17 00:00:00 2001 From: Richard Marmorstein Date: Tue, 27 Feb 2024 09:03:38 -0800 Subject: [PATCH 7/7] Remove dead code --- stripe/_http_client.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/stripe/_http_client.py b/stripe/_http_client.py index 42b8fed0e..3d0a35735 100644 --- a/stripe/_http_client.py +++ b/stripe/_http_client.py @@ -1377,8 +1377,6 @@ def __init__( "Unexpected: tried to initialize AIOHTTPClient but the aiohttp module is not present." ) - self.httpx = httpx - kwargs = {} if self._verify_ssl_certs: ssl_context = ssl.create_default_context(