diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 87691c28..f9cc0f11 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -55,7 +55,7 @@ jobs: make mototest - name: Upload coverage to Codecov if: matrix.python-version == '3.11' - uses: codecov/codecov-action@v3.1.5 + uses: codecov/codecov-action@v4.1.0 with: token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos files: ./coverage.xml diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 00000000..55b19858 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,17 @@ +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.8" + +sphinx: + configuration: docs/conf.py + +python: + install: + - requirements: docs/requirements.txt + +formats: + - htmlzip + - epub diff --git a/CHANGES.rst b/CHANGES.rst index 04f4e65a..9e10c1ed 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,14 @@ Changes ------- +2.12.1 (2024-03-04) +^^^^^^^^^^^^^^^^^^^ +* fix use of proxies #1070 + +2.12.0 (2024-02-28) +^^^^^^^^^^^^^^^^^^^ +* bump botocore dependency specification + 2.11.2 (2024-02-02) ^^^^^^^^^^^^^^^^^^^ * bump botocore dependency specification diff --git a/aiobotocore/__init__.py b/aiobotocore/__init__.py index ded7bd35..524297b5 100644 --- a/aiobotocore/__init__.py +++ b/aiobotocore/__init__.py @@ -1 +1 @@ -__version__ = '2.11.2' +__version__ = '2.12.1' diff --git a/aiobotocore/credentials.py b/aiobotocore/credentials.py index b78d2158..885b71c8 100644 --- a/aiobotocore/credentials.py +++ b/aiobotocore/credentials.py @@ -914,8 +914,7 @@ async def _retrieve_or_fail(self): full_uri = self._fetcher.full_url(self._environ[self.ENV_VAR]) else: full_uri = self._environ[self.ENV_VAR_FULL] - headers = self._build_headers() - fetcher = self._create_fetcher(full_uri, headers) + fetcher = self._create_fetcher(full_uri) creds = await fetcher() return AioRefreshableCredentials( access_key=creds['access_key'], @@ -926,9 +925,10 @@ async def _retrieve_or_fail(self): refresh_using=fetcher, ) - def _create_fetcher(self, full_uri, headers): + def _create_fetcher(self, full_uri, *args, **kwargs): async def fetch_creds(): try: + headers = self._build_headers() response = await self._fetcher.retrieve_full_uri( full_uri, headers=headers ) diff --git a/aiobotocore/httpsession.py b/aiobotocore/httpsession.py index b98c59ed..c8c3fe41 100644 --- a/aiobotocore/httpsession.py +++ b/aiobotocore/httpsession.py @@ -1,4 +1,5 @@ import asyncio +import contextlib import io import os import socket @@ -54,8 +55,11 @@ def __init__( proxies_config=None, connector_args=None, ): + self._exit_stack = contextlib.AsyncExitStack() + # TODO: handle socket_options - self._session: Optional[aiohttp.ClientSession] = None + # keep track of sessions by proxy url (if any) + self._sessions: Dict[Optional[str], aiohttp.ClientSession] = {} self._verify = verify self._proxy_config = ProxyConfiguration( proxies=proxies, proxies_settings=proxies_config @@ -93,53 +97,17 @@ def __init__( # it also pools by host so we don't need a manager, and can pass proxy via # request so don't need proxy manager - ssl_context = None - if bool(verify): - if proxies: - proxies_settings = self._proxy_config.settings - ssl_context = self._setup_proxy_ssl_context(proxies_settings) - # TODO: add support for - # proxies_settings.get('proxy_use_forwarding_for_https') - else: - ssl_context = self._get_ssl_context() - - # inline self._setup_ssl_cert - ca_certs = get_cert_path(verify) - if ca_certs: - ssl_context.load_verify_locations(ca_certs, None, None) - - self._create_connector = lambda: aiohttp.TCPConnector( - limit=max_pool_connections, - verify_ssl=bool(verify), - ssl=ssl_context, - **self._connector_args - ) - self._connector = None - async def __aenter__(self): - assert not self._session and not self._connector + assert not self._sessions - self._connector = self._create_connector() - - self._session = aiohttp.ClientSession( - connector=self._connector, - timeout=self._timeout, - skip_auto_headers={'CONTENT-TYPE'}, - auto_decompress=False, - ) return self async def __aexit__(self, exc_type, exc_val, exc_tb): - if self._session: - await self._session.__aexit__(exc_type, exc_val, exc_tb) - self._session = None - self._connector = None + self._sessions.clear() + await self._exit_stack.aclose() def _get_ssl_context(self): - ssl_context = create_urllib3_context() - if self._cert_file: - ssl_context.load_cert_chain(self._cert_file, self._key_file) - return ssl_context + return create_urllib3_context() def _setup_proxy_ssl_context(self, proxy_url): proxies_settings = self._proxy_config.settings @@ -167,6 +135,58 @@ def _setup_proxy_ssl_context(self, proxy_url): except (OSError, LocationParseError) as e: raise InvalidProxiesConfigError(error=e) + def _chunked(self, headers): + transfer_encoding = headers.get('Transfer-Encoding', '') + if chunked := transfer_encoding.lower() == 'chunked': + # aiohttp wants chunking as a param, and not a header + del headers['Transfer-Encoding'] + return chunked or None + + def _create_connector(self, proxy_url): + ssl_context = None + if bool(self._verify): + if proxy_url: + ssl_context = self._setup_proxy_ssl_context(proxy_url) + # TODO: add support for + # proxies_settings.get('proxy_use_forwarding_for_https') + else: + ssl_context = self._get_ssl_context() + + if ssl_context: + if self._cert_file: + ssl_context.load_cert_chain( + self._cert_file, + self._key_file, + ) + + # inline self._setup_ssl_cert + ca_certs = get_cert_path(self._verify) + if ca_certs: + ssl_context.load_verify_locations(ca_certs, None, None) + + return aiohttp.TCPConnector( + limit=self._max_pool_connections, + verify_ssl=bool(self._verify), + ssl=ssl_context, + **self._connector_args, + ) + + async def _get_session(self, proxy_url): + if not (session := self._sessions.get(proxy_url)): + connector = self._create_connector(proxy_url) + self._sessions[ + proxy_url + ] = session = await self._exit_stack.enter_async_context( + aiohttp.ClientSession( + connector=connector, + timeout=self._timeout, + skip_auto_headers={'CONTENT-TYPE'}, + auto_decompress=False, + ), + ) + + return session + async def close(self): await self.__aexit__(None, None, None) @@ -195,20 +215,15 @@ async def send(self, request): # https://github.com/boto/botocore/issues/1255 headers_['Accept-Encoding'] = 'identity' - chunked = None - if headers_.get('Transfer-Encoding', '').lower() == 'chunked': - # aiohttp wants chunking as a param, and not a header - headers_.pop('Transfer-Encoding', '') - chunked = True - if isinstance(data, io.IOBase): data = _IOBaseWrapper(data) url = URL(url, encoded=True) - response = await self._session.request( + session = await self._get_session(proxy_url) + response = await session.request( request.method, url=url, - chunked=chunked, + chunked=self._chunked(headers_), headers=headers_, data=data, proxy=proxy_url, diff --git a/docs/conf.py b/docs/conf.py index f6ca032a..f70c81f2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -89,7 +89,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -123,10 +123,6 @@ 'github_type': 'star', 'github_banner': True, } -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] # -- Options for HTMLHelp output ------------------------------------------ diff --git a/docs/examples/s3/basic_usage.rst b/docs/examples/s3/basic_usage.rst index 3b8d4732..a9aeb496 100644 --- a/docs/examples/s3/basic_usage.rst +++ b/docs/examples/s3/basic_usage.rst @@ -1,5 +1,6 @@ Put, Get and Delete ------------ +------------------- + Simple put, get, delete example for S3 service: .. literalinclude:: ../../../examples/simple.py diff --git a/docs/requirements.txt b/docs/requirements.txt index 4f81946d..5388c883 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1 +1 @@ -sphinx==2.4.1 +sphinx==5.3.0 diff --git a/readthedocs.yml b/readthedocs.yml deleted file mode 100644 index 7a70b110..00000000 --- a/readthedocs.yml +++ /dev/null @@ -1,11 +0,0 @@ -name: aiobotocore -type: sphinx -requirements_file: docs/requirements.txt -build: - image: latest -python: - version: 3.6 - setup_py_install: true -formats: - - htmlzip - - epub diff --git a/setup.py b/setup.py index 4d7fe28f..adf7feba 100644 --- a/setup.py +++ b/setup.py @@ -7,15 +7,15 @@ # NOTE: When updating botocore make sure to update awscli/boto3 versions below install_requires = [ # pegged to also match items in `extras_require` - 'botocore>=1.33.2,<1.34.35', + 'botocore>=1.34.41,<1.34.52', 'aiohttp>=3.7.4.post0,<4.0.0', 'wrapt>=1.10.10, <2.0.0', 'aioitertools>=0.5.1,<1.0.0', ] extras_require = { - 'awscli': ['awscli>=1.31.2,<1.32.35'], - 'boto3': ['boto3>=1.33.2,<1.34.35'], + 'awscli': ['awscli>=1.32.41,<1.32.52'], + 'boto3': ['boto3>=1.34.41,<1.34.52'], } diff --git a/tests/test_patches.py b/tests/test_patches.py index 05d2569a..570fa495 100644 --- a/tests/test_patches.py +++ b/tests/test_patches.py @@ -185,11 +185,9 @@ 'f09731451ff6ba0645dc82e5c7948dfbf781e025', }, BaseClient.get_paginator: { - '3531d5988aaaf0fbb3885044ccee1a693ec2608b', '1c38079de68ccd43a5a06e36b1a47ec62233a7c2', }, BaseClient.get_waiter: { - '44f0473d993d49ac7502984a7ccee3240b088404', '4a4aeabe53af25d3737204187a31f930230864b4', }, BaseClient.__getattr__: {'3ec17f468f50789fa633d6041f40b66a2f593e77'}, @@ -266,10 +264,10 @@ ContainerProvider.__init__: {'ea6aafb2e12730066af930fb5a27f7659c1736a1'}, ContainerProvider.load: {'57c35569050b45c1e9e33fcdb3b49da9e342fdcf'}, ContainerProvider._retrieve_or_fail: { - '7c14f1cdee07217f847a71068866bdd10c3fa0fa' + 'c99153a4c68927810a3edde09ee98c5ba33d3697' }, ContainerProvider._create_fetcher: { - '935ae28fdb1c76f419523d4030265f8c4d9d0b00' + 'a921ee40b9b4779f238adcf369a3757b19857fc7' }, InstanceMetadataProvider.load: { '15becfc0373ccfbc1bb200bd6a34731e61561d06'