From a732d44c9e62c70235788056c92d97c12c5aae50 Mon Sep 17 00:00:00 2001 From: dblock Date: Fri, 20 Oct 2023 15:50:47 -0400 Subject: [PATCH 01/13] WIP: Added support for AWS Sigv4 for UrlLib3. Signed-off-by: dblock --- DEVELOPER_GUIDE.md | 14 ++- opensearchpy/connection/http_urllib3.py | 21 ++++- opensearchpy/helpers/signer.py | 70 ++++++++++++++- test_opensearchpy/test_connection.py | 110 ++++++++++++++++++------ 4 files changed, 184 insertions(+), 31 deletions(-) diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index ec39602f..bdcd8f08 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -45,7 +45,19 @@ docker run -d -p 9200:9200 -p 9600:9600 -e "discovery.type=single-node" opensear Tests require a live instance of OpenSearch running in docker. -This will start a new instance and run tests against the latest version of OpenSearch. +If you have one running. + +``` +python setup.py test +``` + +To run tests in a specific test file. + +``` +python setup.py test -s test_opensearchpy/test_connection.py +``` + +If you want to auto-start one, the following will start a new instance and run tests against the latest version of OpenSearch. ``` ./.ci/run-tests diff --git a/opensearchpy/connection/http_urllib3.py b/opensearchpy/connection/http_urllib3.py index 4bc27bbb..6254ef27 100644 --- a/opensearchpy/connection/http_urllib3.py +++ b/opensearchpy/connection/http_urllib3.py @@ -27,6 +27,7 @@ import ssl import time import warnings +from typing import Callable import urllib3 # type: ignore from urllib3.exceptions import ReadTimeoutError @@ -128,14 +129,21 @@ def __init__( opaque_id=opaque_id, **kwargs ) - if http_auth is not None: - if isinstance(http_auth, (tuple, list)): - http_auth = ":".join(http_auth) - self.headers.update(urllib3.make_headers(basic_auth=http_auth)) pool_class = urllib3.HTTPConnectionPool kw = {} + self.http_auth = http_auth + if self.http_auth is not None: + if isinstance(self.http_auth, Callable): + pass + elif isinstance(self.http_auth, (tuple, list)): + self.headers.update( + urllib3.make_headers(basic_auth=":".join(http_auth)) + ) + else: + self.headers.update(urllib3.make_headers(basic_auth=http_auth)) + # if providing an SSL context, raise error if any other SSL related flag is used if ssl_context and ( (verify_certs is not VERIFY_CERTS_DEFAULT) @@ -218,6 +226,7 @@ def perform_request( url = "%s?%s" % (url, urlencode(params)) full_url = self.host + url + start = time.time() orig_body = body try: @@ -240,6 +249,10 @@ def perform_request( body = self._gzip_compress(body) request_headers["content-encoding"] = "gzip" + if self.http_auth is not None: + if isinstance(self.http_auth, Callable): + request_headers.update(self.http_auth(method, url, body)) + response = self.pool.urlopen( method, url, body, retries=Retry(False), headers=request_headers, **kw ) diff --git a/opensearchpy/helpers/signer.py b/opensearchpy/helpers/signer.py index 176f6ac9..c171e5b2 100644 --- a/opensearchpy/helpers/signer.py +++ b/opensearchpy/helpers/signer.py @@ -8,6 +8,7 @@ # GitHub history for details. import sys +from typing import Callable import requests @@ -43,7 +44,7 @@ def fetch_url(prepared_request): # type: ignore return url.scheme + "://" + location + path + querystring -class AWSV4SignerAuth(requests.auth.AuthBase): +class RequestsAWSV4SignerAuth(requests.auth.AuthBase): """ AWS V4 Request Signer for Requests. """ @@ -107,3 +108,70 @@ def _sign_request(self, prepared_request): # type: ignore ) return prepared_request + + +# Deprecated: use RequestsAWSV4SignerAuth +class AWSV4SignerAuth(RequestsAWSV4SignerAuth): + pass + + +class UrlLib3AWSV4SignerAuth(Callable): # type: ignore + """ + AWS V4 Request Signer for UrLLib3. + """ + + def __init__(self, credentials, region, service="es"): # type: ignore + if not credentials: + raise ValueError("Credentials cannot be empty") + self.credentials = credentials + + if not region: + raise ValueError("Region cannot be empty") + self.region = region + + if not service: + raise ValueError("Service name cannot be empty") + self.service = service + + def __call__(self, method, url, body): # type: ignore + return self._sign_request(method, url, body) # type: ignore + + def _sign_request(self, method, url, body): # type: ignore + """ + This method helps in signing the request by injecting the required headers. + :param prepared_request: unsigned request + :return: signed request + """ + + from botocore.auth import SigV4Auth + from botocore.awsrequest import AWSRequest + + # create an AWS request object and sign it using SigV4Auth + aws_request = AWSRequest( + method=method.upper(), + url=url, + data=body, + ) + + # credentials objects expose access_key, secret_key and token attributes + # via @property annotations that call _refresh() on every access, + # creating a race condition if the credentials expire before secret_key + # is called but after access_key- the end result is the access_key doesn't + # correspond to the secret_key used to sign the request. To avoid this, + # get_frozen_credentials() which returns non-refreshing credentials is + # called if it exists. + credentials = ( + self.credentials.get_frozen_credentials() + if hasattr(self.credentials, "get_frozen_credentials") + and callable(self.credentials.get_frozen_credentials) + else self.credentials + ) + + sig_v4_auth = SigV4Auth(credentials, self.service, self.region) + sig_v4_auth.add_auth(aws_request) + + # copy the headers from AWS request object into the prepared_request + headers = dict(aws_request.headers.items()) + headers["X-Amz-Content-SHA256"] = sig_v4_auth.payload(aws_request) + + return headers diff --git a/test_opensearchpy/test_connection.py b/test_opensearchpy/test_connection.py index 5ec6e09d..d2b2a73b 100644 --- a/test_opensearchpy/test_connection.py +++ b/test_opensearchpy/test_connection.py @@ -328,19 +328,14 @@ def test_http_auth_list(self): def test_aws_signer_as_http_auth(self): region = "us-west-2" - import requests - - from opensearchpy.helpers.signer import AWSV4SignerAuth + from opensearchpy.helpers.signer import UrlLib3AWSV4SignerAuth - auth = AWSV4SignerAuth(self.mock_session(), region) - con = RequestsHttpConnection(http_auth=auth) - prepared_request = requests.Request("GET", "http://localhost").prepare() - auth(prepared_request) - self.assertEqual(auth, con.session.auth) - self.assertIn("Authorization", prepared_request.headers) - self.assertIn("X-Amz-Date", prepared_request.headers) - self.assertIn("X-Amz-Security-Token", prepared_request.headers) - self.assertIn("X-Amz-Content-SHA256", prepared_request.headers) + auth = UrlLib3AWSV4SignerAuth(self.mock_session(), region) + headers = auth("GET", "http://localhost", None) + self.assertIn("Authorization", headers) + self.assertIn("X-Amz-Date", headers) + self.assertIn("X-Amz-Security-Token", headers) + self.assertIn("X-Amz-Content-SHA256", headers) @pytest.mark.skipif( sys.version_info < (3, 6), reason="AWSV4SignerAuth requires python3.6+" @@ -381,18 +376,13 @@ def test_aws_signer_when_service_is_specified(self): region = "us-west-1" service = "aoss" - import requests + from opensearchpy.helpers.signer import UrlLib3AWSV4SignerAuth - from opensearchpy.helpers.signer import AWSV4SignerAuth - - auth = AWSV4SignerAuth(self.mock_session(), region, service) - con = RequestsHttpConnection(http_auth=auth) - prepared_request = requests.Request("GET", "http://localhost").prepare() - auth(prepared_request) - self.assertEqual(auth, con.session.auth) - self.assertIn("Authorization", prepared_request.headers) - self.assertIn("X-Amz-Date", prepared_request.headers) - self.assertIn("X-Amz-Security-Token", prepared_request.headers) + auth = UrlLib3AWSV4SignerAuth(self.mock_session(), region, service) + headers = auth("GET", "http://localhost", None) + self.assertIn("Authorization", headers) + self.assertIn("X-Amz-Date", headers) + self.assertIn("X-Amz-Security-Token", headers) def mock_session(self): access_key = uuid.uuid4().hex @@ -520,7 +510,25 @@ def mock_session(self): @pytest.mark.skipif( sys.version_info < (3, 6), reason="AWSV4SignerAuth requires python3.6+" ) - def test_aws_signer_frozen_credentials_as_http_auth(self): + def test_urllib3_http_connection_aws_signer_frozen_credentials_as_http_auth(self): + region = "us-west-2" + + from opensearchpy.helpers.signer import UrlLib3AWSV4SignerAuth + + mock_session = self.mock_session() + + auth = UrlLib3AWSV4SignerAuth(mock_session, region) + headers = auth("GET", "http://localhost", None) + self.assertIn("Authorization", headers) + self.assertIn("X-Amz-Date", headers) + self.assertIn("X-Amz-Security-Token", headers) + self.assertIn("X-Amz-Content-SHA256", headers) + mock_session.get_frozen_credentials.assert_called_once() + + @pytest.mark.skipif( + sys.version_info < (3, 6), reason="AWSV4SignerAuth requires python3.6+" + ) + def test_requests_http_connection_aws_signer_frozen_credentials_as_http_auth(self): region = "us-west-2" import requests @@ -541,7 +549,7 @@ def test_aws_signer_frozen_credentials_as_http_auth(self): mock_session.get_frozen_credentials.assert_called_once() -class TestRequestsConnection(TestCase): +class TestRequestsHttpConnection(TestCase): def _get_mock_connection( self, connection_params={}, status_code=200, response_body=b"{}" ): @@ -898,6 +906,58 @@ def send_raise(*_, **__): conn.perform_request("GET", "/") assert str(e.value) == "Wasn't modified!" + def mock_session(self): + access_key = uuid.uuid4().hex + secret_key = uuid.uuid4().hex + token = uuid.uuid4().hex + dummy_session = Mock() + dummy_session.access_key = access_key + dummy_session.secret_key = secret_key + dummy_session.token = token + del dummy_session.get_frozen_credentials + + return dummy_session + + @pytest.mark.skipif( + sys.version_info < (3, 6), reason="AWSV4SignerAuth requires python3.6+" + ) + def test_aws_signer_as_http_auth(self): + region = "us-west-2" + + import requests + + from opensearchpy.helpers.signer import AWSV4SignerAuth + + auth = AWSV4SignerAuth(self.mock_session(), region) + con = RequestsHttpConnection(http_auth=auth) + prepared_request = requests.Request("GET", "http://localhost").prepare() + auth(prepared_request) + self.assertEqual(auth, con.session.auth) + self.assertIn("Authorization", prepared_request.headers) + self.assertIn("X-Amz-Date", prepared_request.headers) + self.assertIn("X-Amz-Security-Token", prepared_request.headers) + self.assertIn("X-Amz-Content-SHA256", prepared_request.headers) + + @pytest.mark.skipif( + sys.version_info < (3, 6), reason="AWSV4SignerAuth requires python3.6+" + ) + def test_aws_signer_when_service_is_specified(self): + region = "us-west-1" + service = "aoss" + + import requests + + from opensearchpy.helpers.signer import AWSV4SignerAuth + + auth = AWSV4SignerAuth(self.mock_session(), region, service) + con = RequestsHttpConnection(http_auth=auth) + prepared_request = requests.Request("GET", "http://localhost").prepare() + auth(prepared_request) + self.assertEqual(auth, con.session.auth) + self.assertIn("Authorization", prepared_request.headers) + self.assertIn("X-Amz-Date", prepared_request.headers) + self.assertIn("X-Amz-Security-Token", prepared_request.headers) + @pytest.mark.skipif( sys.version_info < (3, 0), From e18b7038bb71086c08bb77dde8410ccabc942984 Mon Sep 17 00:00:00 2001 From: dblock Date: Fri, 20 Oct 2023 17:29:43 -0400 Subject: [PATCH 02/13] Refactored common implementation. Signed-off-by: dblock --- opensearchpy/__init__.py | 9 +- opensearchpy/connection/http_urllib3.py | 8 +- opensearchpy/helpers/__init__.py | 4 +- opensearchpy/helpers/__init__.pyi | 1 + opensearchpy/helpers/signer.py | 108 +++++++++--------------- 5 files changed, 54 insertions(+), 76 deletions(-) diff --git a/opensearchpy/__init__.py b/opensearchpy/__init__.py index 6669f179..30751faa 100644 --- a/opensearchpy/__init__.py +++ b/opensearchpy/__init__.py @@ -71,7 +71,12 @@ UnknownDslObject, ValidationException, ) -from .helpers import AWSV4SignerAsyncAuth, AWSV4SignerAuth +from .helpers import ( + AWSV4SignerAsyncAuth, + AWSV4SignerAuth, + RequestsAWSV4SignerAuth, + UrlLib3AWSV4SignerAuth, +) from .helpers.aggs import A from .helpers.analysis import analyzer, char_filter, normalizer, token_filter, tokenizer from .helpers.document import Document, InnerDoc, MetaField @@ -166,6 +171,8 @@ "OpenSearchWarning", "OpenSearchDeprecationWarning", "AWSV4SignerAuth", + "UrlLib3AWSV4SignerAuth", + "RequestsAWSV4SignerAuth", "AWSV4SignerAsyncAuth", "A", "AttrDict", diff --git a/opensearchpy/connection/http_urllib3.py b/opensearchpy/connection/http_urllib3.py index 6254ef27..0042cc3c 100644 --- a/opensearchpy/connection/http_urllib3.py +++ b/opensearchpy/connection/http_urllib3.py @@ -130,9 +130,6 @@ def __init__( **kwargs ) - pool_class = urllib3.HTTPConnectionPool - kw = {} - self.http_auth = http_auth if self.http_auth is not None: if isinstance(self.http_auth, Callable): @@ -144,6 +141,9 @@ def __init__( else: self.headers.update(urllib3.make_headers(basic_auth=http_auth)) + pool_class = urllib3.HTTPConnectionPool + kw = {} + # if providing an SSL context, raise error if any other SSL related flag is used if ssl_context and ( (verify_certs is not VERIFY_CERTS_DEFAULT) @@ -251,7 +251,7 @@ def perform_request( if self.http_auth is not None: if isinstance(self.http_auth, Callable): - request_headers.update(self.http_auth(method, url, body)) + request_headers.update(self.http_auth(method, full_url, body)) response = self.pool.urlopen( method, url, body, retries=Retry(False), headers=request_headers, **kw diff --git a/opensearchpy/helpers/__init__.py b/opensearchpy/helpers/__init__.py index 72a7d140..95f92e34 100644 --- a/opensearchpy/helpers/__init__.py +++ b/opensearchpy/helpers/__init__.py @@ -39,7 +39,7 @@ ) from .asyncsigner import AWSV4SignerAsyncAuth from .errors import BulkIndexError, ScanError -from .signer import AWSV4SignerAuth +from .signer import AWSV4SignerAuth, RequestsAWSV4SignerAuth, UrlLib3AWSV4SignerAuth __all__ = [ "BulkIndexError", @@ -54,6 +54,8 @@ "_process_bulk_chunk", "AWSV4SignerAuth", "AWSV4SignerAsyncAuth", + "RequestsAWSV4SignerAuth", + "UrlLib3AWSV4SignerAuth", ] diff --git a/opensearchpy/helpers/__init__.pyi b/opensearchpy/helpers/__init__.pyi index 59b5cefd..784ac030 100644 --- a/opensearchpy/helpers/__init__.pyi +++ b/opensearchpy/helpers/__init__.pyi @@ -48,5 +48,6 @@ try: from .._async.helpers.actions import async_streaming_bulk as async_streaming_bulk from .asyncsigner import AWSV4SignerAsyncAuth as AWSV4SignerAsyncAuth from .signer import AWSV4SignerAuth as AWSV4SignerAuth + from .signer import RequestsAWSV4SignerAuth, UrlLib3AWSV4SignerAuth except (ImportError, SyntaxError): pass diff --git a/opensearchpy/helpers/signer.py b/opensearchpy/helpers/signer.py index c171e5b2..0eeba0b8 100644 --- a/opensearchpy/helpers/signer.py +++ b/opensearchpy/helpers/signer.py @@ -8,7 +8,7 @@ # GitHub history for details. import sys -from typing import Callable +from typing import Any, Callable import requests @@ -44,12 +44,12 @@ def fetch_url(prepared_request): # type: ignore return url.scheme + "://" + location + path + querystring -class RequestsAWSV4SignerAuth(requests.auth.AuthBase): +class AuthSigner: """ - AWS V4 Request Signer for Requests. + Generic AWS V4 Request Signer. """ - def __init__(self, credentials, region, service="es"): # type: ignore + def __init__(self, credentials, region: str, service: str = "es") -> Any: # type: ignore if not credentials: raise ValueError("Credentials cannot be empty") self.credentials = credentials @@ -62,27 +62,22 @@ def __init__(self, credentials, region, service="es"): # type: ignore raise ValueError("Service name cannot be empty") self.service = service - def __call__(self, request): # type: ignore - return self._sign_request(request) # type: ignore + def sign(self, method: str, url: str, body: Any) -> dict[str, str]: + print(f"SIGN: {method}: {url} ({body})") - def _sign_request(self, prepared_request): # type: ignore """ - This method helps in signing the request by injecting the required headers. - :param prepared_request: unsigned request - :return: signed request + This method signs the request and returns headers. + :param method: HTTP method + :param url: url + :param body: body + :return: headers """ from botocore.auth import SigV4Auth from botocore.awsrequest import AWSRequest - url = fetch_url(prepared_request) # type: ignore - # create an AWS request object and sign it using SigV4Auth - aws_request = AWSRequest( - method=prepared_request.method.upper(), - url=url, - data=prepared_request.body, - ) + aws_request = AWSRequest(method=method.upper(), url=url, data=body) # credentials objects expose access_key, secret_key and token attributes # via @property annotations that call _refresh() on every access, @@ -102,76 +97,49 @@ def _sign_request(self, prepared_request): # type: ignore sig_v4_auth.add_auth(aws_request) # copy the headers from AWS request object into the prepared_request - prepared_request.headers.update(dict(aws_request.headers.items())) - prepared_request.headers["X-Amz-Content-SHA256"] = sig_v4_auth.payload( - aws_request - ) - - return prepared_request - + headers = dict(aws_request.headers.items()) + headers["X-Amz-Content-SHA256"] = sig_v4_auth.payload(aws_request) -# Deprecated: use RequestsAWSV4SignerAuth -class AWSV4SignerAuth(RequestsAWSV4SignerAuth): - pass + return headers -class UrlLib3AWSV4SignerAuth(Callable): # type: ignore +class RequestsAWSV4SignerAuth(requests.auth.AuthBase): """ - AWS V4 Request Signer for UrLLib3. + AWS V4 Request Signer for Requests. """ def __init__(self, credentials, region, service="es"): # type: ignore - if not credentials: - raise ValueError("Credentials cannot be empty") - self.credentials = credentials - - if not region: - raise ValueError("Region cannot be empty") - self.region = region - - if not service: - raise ValueError("Service name cannot be empty") - self.service = service + self.signer = AuthSigner(credentials, region, service) - def __call__(self, method, url, body): # type: ignore - return self._sign_request(method, url, body) # type: ignore + def __call__(self, request): # type: ignore + return self._sign_request(request) # type: ignore - def _sign_request(self, method, url, body): # type: ignore + def _sign_request(self, prepared_request): # type: ignore """ This method helps in signing the request by injecting the required headers. :param prepared_request: unsigned request :return: signed request """ - from botocore.auth import SigV4Auth - from botocore.awsrequest import AWSRequest - - # create an AWS request object and sign it using SigV4Auth - aws_request = AWSRequest( - method=method.upper(), - url=url, - data=body, + prepared_request.headers.update( + self.signer.sign( + prepared_request.method, + fetch_url(prepared_request), # type: ignore + prepared_request.body, + ) ) - # credentials objects expose access_key, secret_key and token attributes - # via @property annotations that call _refresh() on every access, - # creating a race condition if the credentials expire before secret_key - # is called but after access_key- the end result is the access_key doesn't - # correspond to the secret_key used to sign the request. To avoid this, - # get_frozen_credentials() which returns non-refreshing credentials is - # called if it exists. - credentials = ( - self.credentials.get_frozen_credentials() - if hasattr(self.credentials, "get_frozen_credentials") - and callable(self.credentials.get_frozen_credentials) - else self.credentials - ) + return prepared_request - sig_v4_auth = SigV4Auth(credentials, self.service, self.region) - sig_v4_auth.add_auth(aws_request) - # copy the headers from AWS request object into the prepared_request - headers = dict(aws_request.headers.items()) - headers["X-Amz-Content-SHA256"] = sig_v4_auth.payload(aws_request) +# Deprecated: use RequestsAWSV4SignerAuth +class AWSV4SignerAuth(RequestsAWSV4SignerAuth): + pass - return headers + +class UrlLib3AWSV4SignerAuth(Callable): # type: ignore + def __init__(self, credentials, region, service="es"): # type: ignore + self.signer = AuthSigner(credentials, region, service) + + def __call__(self, method: str, url: str, body: Any) -> dict[str, str]: + return self.signer.sign(method, url, body) From fd9fa734b17d61937703977f88f22a272d14f061 Mon Sep 17 00:00:00 2001 From: dblock Date: Fri, 20 Oct 2023 17:30:26 -0400 Subject: [PATCH 03/13] Added sigv4 samples. Signed-off-by: dblock --- samples/poetry.lock | 688 ++++++++------------------------ samples/pyproject.toml | 3 +- samples/sigv4/README.md | 22 + samples/sigv4/requests-hello.py | 69 ++++ samples/sigv4/urllib3-hello.py | 69 ++++ 5 files changed, 320 insertions(+), 531 deletions(-) create mode 100644 samples/sigv4/README.md create mode 100644 samples/sigv4/requests-hello.py create mode 100644 samples/sigv4/urllib3-hello.py diff --git a/samples/poetry.lock b/samples/poetry.lock index e8e8b7cc..55fb558d 100644 --- a/samples/poetry.lock +++ b/samples/poetry.lock @@ -1,174 +1,45 @@ # This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. [[package]] -name = "aiohttp" -version = "3.8.5" -description = "Async http client/server framework (asyncio)" +name = "boto3" +version = "1.28.67" +description = "The AWS SDK for Python" optional = false -python-versions = ">=3.6" +python-versions = ">= 3.7" files = [ - {file = "aiohttp-3.8.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a94159871304770da4dd371f4291b20cac04e8c94f11bdea1c3478e557fbe0d8"}, - {file = "aiohttp-3.8.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:13bf85afc99ce6f9ee3567b04501f18f9f8dbbb2ea11ed1a2e079670403a7c84"}, - {file = "aiohttp-3.8.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ce2ac5708501afc4847221a521f7e4b245abf5178cf5ddae9d5b3856ddb2f3a"}, - {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96943e5dcc37a6529d18766597c491798b7eb7a61d48878611298afc1fca946c"}, - {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ad5c3c4590bb3cc28b4382f031f3783f25ec223557124c68754a2231d989e2b"}, - {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c413c633d0512df4dc7fd2373ec06cc6a815b7b6d6c2f208ada7e9e93a5061d"}, - {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df72ac063b97837a80d80dec8d54c241af059cc9bb42c4de68bd5b61ceb37caa"}, - {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c48c5c0271149cfe467c0ff8eb941279fd6e3f65c9a388c984e0e6cf57538e14"}, - {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:368a42363c4d70ab52c2c6420a57f190ed3dfaca6a1b19afda8165ee16416a82"}, - {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7607ec3ce4993464368505888af5beb446845a014bc676d349efec0e05085905"}, - {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0d21c684808288a98914e5aaf2a7c6a3179d4df11d249799c32d1808e79503b5"}, - {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:312fcfbacc7880a8da0ae8b6abc6cc7d752e9caa0051a53d217a650b25e9a691"}, - {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ad093e823df03bb3fd37e7dec9d4670c34f9e24aeace76808fc20a507cace825"}, - {file = "aiohttp-3.8.5-cp310-cp310-win32.whl", hash = "sha256:33279701c04351a2914e1100b62b2a7fdb9a25995c4a104259f9a5ead7ed4802"}, - {file = "aiohttp-3.8.5-cp310-cp310-win_amd64.whl", hash = "sha256:6e4a280e4b975a2e7745573e3fc9c9ba0d1194a3738ce1cbaa80626cc9b4f4df"}, - {file = "aiohttp-3.8.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae871a964e1987a943d83d6709d20ec6103ca1eaf52f7e0d36ee1b5bebb8b9b9"}, - {file = "aiohttp-3.8.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:461908b2578955045efde733719d62f2b649c404189a09a632d245b445c9c975"}, - {file = "aiohttp-3.8.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72a860c215e26192379f57cae5ab12b168b75db8271f111019509a1196dfc780"}, - {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc14be025665dba6202b6a71cfcdb53210cc498e50068bc088076624471f8bb9"}, - {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8af740fc2711ad85f1a5c034a435782fbd5b5f8314c9a3ef071424a8158d7f6b"}, - {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:841cd8233cbd2111a0ef0a522ce016357c5e3aff8a8ce92bcfa14cef890d698f"}, - {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ed1c46fb119f1b59304b5ec89f834f07124cd23ae5b74288e364477641060ff"}, - {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84f8ae3e09a34f35c18fa57f015cc394bd1389bce02503fb30c394d04ee6b938"}, - {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62360cb771707cb70a6fd114b9871d20d7dd2163a0feafe43fd115cfe4fe845e"}, - {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:23fb25a9f0a1ca1f24c0a371523546366bb642397c94ab45ad3aedf2941cec6a"}, - {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0ba0d15164eae3d878260d4c4df859bbdc6466e9e6689c344a13334f988bb53"}, - {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5d20003b635fc6ae3f96d7260281dfaf1894fc3aa24d1888a9b2628e97c241e5"}, - {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0175d745d9e85c40dcc51c8f88c74bfbaef9e7afeeeb9d03c37977270303064c"}, - {file = "aiohttp-3.8.5-cp311-cp311-win32.whl", hash = "sha256:2e1b1e51b0774408f091d268648e3d57f7260c1682e7d3a63cb00d22d71bb945"}, - {file = "aiohttp-3.8.5-cp311-cp311-win_amd64.whl", hash = "sha256:043d2299f6dfdc92f0ac5e995dfc56668e1587cea7f9aa9d8a78a1b6554e5755"}, - {file = "aiohttp-3.8.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cae533195e8122584ec87531d6df000ad07737eaa3c81209e85c928854d2195c"}, - {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f21e83f355643c345177a5d1d8079f9f28b5133bcd154193b799d380331d5d3"}, - {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7a75ef35f2df54ad55dbf4b73fe1da96f370e51b10c91f08b19603c64004acc"}, - {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e2e9839e14dd5308ee773c97115f1e0a1cb1d75cbeeee9f33824fa5144c7634"}, - {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44e65da1de4403d0576473e2344828ef9c4c6244d65cf4b75549bb46d40b8dd"}, - {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d847e4cde6ecc19125ccbc9bfac4a7ab37c234dd88fbb3c5c524e8e14da543"}, - {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:c7a815258e5895d8900aec4454f38dca9aed71085f227537208057853f9d13f2"}, - {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:8b929b9bd7cd7c3939f8bcfffa92fae7480bd1aa425279d51a89327d600c704d"}, - {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:5db3a5b833764280ed7618393832e0853e40f3d3e9aa128ac0ba0f8278d08649"}, - {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:a0215ce6041d501f3155dc219712bc41252d0ab76474615b9700d63d4d9292af"}, - {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:fd1ed388ea7fbed22c4968dd64bab0198de60750a25fe8c0c9d4bef5abe13824"}, - {file = "aiohttp-3.8.5-cp36-cp36m-win32.whl", hash = "sha256:6e6783bcc45f397fdebc118d772103d751b54cddf5b60fbcc958382d7dd64f3e"}, - {file = "aiohttp-3.8.5-cp36-cp36m-win_amd64.whl", hash = "sha256:b5411d82cddd212644cf9360879eb5080f0d5f7d809d03262c50dad02f01421a"}, - {file = "aiohttp-3.8.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:01d4c0c874aa4ddfb8098e85d10b5e875a70adc63db91f1ae65a4b04d3344cda"}, - {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5980a746d547a6ba173fd5ee85ce9077e72d118758db05d229044b469d9029a"}, - {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a482e6da906d5e6e653be079b29bc173a48e381600161c9932d89dfae5942ef"}, - {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80bd372b8d0715c66c974cf57fe363621a02f359f1ec81cba97366948c7fc873"}, - {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1161b345c0a444ebcf46bf0a740ba5dcf50612fd3d0528883fdc0eff578006a"}, - {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd56db019015b6acfaaf92e1ac40eb8434847d9bf88b4be4efe5bfd260aee692"}, - {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:153c2549f6c004d2754cc60603d4668899c9895b8a89397444a9c4efa282aaf4"}, - {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4a01951fabc4ce26ab791da5f3f24dca6d9a6f24121746eb19756416ff2d881b"}, - {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bfb9162dcf01f615462b995a516ba03e769de0789de1cadc0f916265c257e5d8"}, - {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:7dde0009408969a43b04c16cbbe252c4f5ef4574ac226bc8815cd7342d2028b6"}, - {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4149d34c32f9638f38f544b3977a4c24052042affa895352d3636fa8bffd030a"}, - {file = "aiohttp-3.8.5-cp37-cp37m-win32.whl", hash = "sha256:68c5a82c8779bdfc6367c967a4a1b2aa52cd3595388bf5961a62158ee8a59e22"}, - {file = "aiohttp-3.8.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2cf57fb50be5f52bda004b8893e63b48530ed9f0d6c96c84620dc92fe3cd9b9d"}, - {file = "aiohttp-3.8.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:eca4bf3734c541dc4f374ad6010a68ff6c6748f00451707f39857f429ca36ced"}, - {file = "aiohttp-3.8.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1274477e4c71ce8cfe6c1ec2f806d57c015ebf84d83373676036e256bc55d690"}, - {file = "aiohttp-3.8.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:28c543e54710d6158fc6f439296c7865b29e0b616629767e685a7185fab4a6b9"}, - {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:910bec0c49637d213f5d9877105d26e0c4a4de2f8b1b29405ff37e9fc0ad52b8"}, - {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5443910d662db951b2e58eb70b0fbe6b6e2ae613477129a5805d0b66c54b6cb7"}, - {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e460be6978fc24e3df83193dc0cc4de46c9909ed92dd47d349a452ef49325b7"}, - {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb1558def481d84f03b45888473fc5a1f35747b5f334ef4e7a571bc0dfcb11f8"}, - {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34dd0c107799dcbbf7d48b53be761a013c0adf5571bf50c4ecad5643fe9cfcd0"}, - {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aa1990247f02a54185dc0dff92a6904521172a22664c863a03ff64c42f9b5410"}, - {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0e584a10f204a617d71d359fe383406305a4b595b333721fa50b867b4a0a1548"}, - {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:a3cf433f127efa43fee6b90ea4c6edf6c4a17109d1d037d1a52abec84d8f2e42"}, - {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:c11f5b099adafb18e65c2c997d57108b5bbeaa9eeee64a84302c0978b1ec948b"}, - {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:84de26ddf621d7ac4c975dbea4c945860e08cccde492269db4e1538a6a6f3c35"}, - {file = "aiohttp-3.8.5-cp38-cp38-win32.whl", hash = "sha256:ab88bafedc57dd0aab55fa728ea10c1911f7e4d8b43e1d838a1739f33712921c"}, - {file = "aiohttp-3.8.5-cp38-cp38-win_amd64.whl", hash = "sha256:5798a9aad1879f626589f3df0f8b79b3608a92e9beab10e5fda02c8a2c60db2e"}, - {file = "aiohttp-3.8.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a6ce61195c6a19c785df04e71a4537e29eaa2c50fe745b732aa937c0c77169f3"}, - {file = "aiohttp-3.8.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:773dd01706d4db536335fcfae6ea2440a70ceb03dd3e7378f3e815b03c97ab51"}, - {file = "aiohttp-3.8.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f83a552443a526ea38d064588613aca983d0ee0038801bc93c0c916428310c28"}, - {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f7372f7341fcc16f57b2caded43e81ddd18df53320b6f9f042acad41f8e049a"}, - {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea353162f249c8097ea63c2169dd1aa55de1e8fecbe63412a9bc50816e87b761"}, - {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d47ae48db0b2dcf70bc8a3bc72b3de86e2a590fc299fdbbb15af320d2659de"}, - {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d827176898a2b0b09694fbd1088c7a31836d1a505c243811c87ae53a3f6273c1"}, - {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3562b06567c06439d8b447037bb655ef69786c590b1de86c7ab81efe1c9c15d8"}, - {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4e874cbf8caf8959d2adf572a78bba17cb0e9d7e51bb83d86a3697b686a0ab4d"}, - {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6809a00deaf3810e38c628e9a33271892f815b853605a936e2e9e5129762356c"}, - {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:33776e945d89b29251b33a7e7d006ce86447b2cfd66db5e5ded4e5cd0340585c"}, - {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:eaeed7abfb5d64c539e2db173f63631455f1196c37d9d8d873fc316470dfbacd"}, - {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e91d635961bec2d8f19dfeb41a539eb94bd073f075ca6dae6c8dc0ee89ad6f91"}, - {file = "aiohttp-3.8.5-cp39-cp39-win32.whl", hash = "sha256:00ad4b6f185ec67f3e6562e8a1d2b69660be43070bd0ef6fcec5211154c7df67"}, - {file = "aiohttp-3.8.5-cp39-cp39-win_amd64.whl", hash = "sha256:c0a9034379a37ae42dea7ac1e048352d96286626251862e448933c0f59cbd79c"}, - {file = "aiohttp-3.8.5.tar.gz", hash = "sha256:b9552ec52cc147dbf1944ac7ac98af7602e51ea2dcd076ed194ca3c0d1c7d0bc"}, + {file = "boto3-1.28.67-py3-none-any.whl", hash = "sha256:7d17f987a8b4f804e5ae509a30589736a72c6db7b0e2fb1338997128fdc9a3ec"}, + {file = "boto3-1.28.67.tar.gz", hash = "sha256:8db91c0648c9dcde1cf7fb4c15cd50da1fdef573595a9b9c769a303c7531b9a6"}, ] [package.dependencies] -aiosignal = ">=1.1.2" -async-timeout = ">=4.0.0a3,<5.0" -asynctest = {version = "0.13.0", markers = "python_version < \"3.8\""} -attrs = ">=17.3.0" -charset-normalizer = ">=2.0,<4.0" -frozenlist = ">=1.1.1" -multidict = ">=4.5,<7.0" -typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} -yarl = ">=1.0,<2.0" +botocore = ">=1.31.67,<1.32.0" +jmespath = ">=0.7.1,<2.0.0" +s3transfer = ">=0.7.0,<0.8.0" [package.extras] -speedups = ["Brotli", "aiodns", "cchardet"] +crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] -name = "aiosignal" -version = "1.3.1" -description = "aiosignal: a list of registered asynchronous callbacks" +name = "botocore" +version = "1.31.67" +description = "Low-level, data-driven core of boto 3." optional = false -python-versions = ">=3.7" +python-versions = ">= 3.7" files = [ - {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, - {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, + {file = "botocore-1.31.67-py3-none-any.whl", hash = "sha256:487fb6ee4a6612613da370599b1a1aca0e159dd9e94b2e8aaa8e6ad9cc546ded"}, + {file = "botocore-1.31.67.tar.gz", hash = "sha256:ab3b73a2e03efa1c534a94f8db4a5cf45629a53e5478d2d154b0a3e2ffb05249"}, ] [package.dependencies] -frozenlist = ">=1.1.0" - -[[package]] -name = "async-timeout" -version = "4.0.2" -description = "Timeout context manager for asyncio programs" -optional = false -python-versions = ">=3.6" -files = [ - {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, - {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, -] - -[package.dependencies] -typing-extensions = {version = ">=3.6.5", markers = "python_version < \"3.8\""} - -[[package]] -name = "asynctest" -version = "0.13.0" -description = "Enhance the standard unittest package with features for testing asyncio libraries" -optional = false -python-versions = ">=3.5" -files = [ - {file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"}, - {file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"}, -] - -[[package]] -name = "attrs" -version = "23.1.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.7" -files = [ - {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, - {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, +jmespath = ">=0.7.1,<2.0.0" +python-dateutil = ">=2.1,<3.0.0" +urllib3 = [ + {version = ">=1.25.4,<1.27", markers = "python_version < \"3.10\""}, + {version = ">=1.25.4,<2.1", markers = "python_version >= \"3.10\""}, ] -[package.dependencies] -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} - [package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[docs,tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +crt = ["awscrt (==0.16.26)"] [[package]] name = "certifi" @@ -183,169 +54,101 @@ files = [ [[package]] name = "charset-normalizer" -version = "3.2.0" +version = "3.3.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"}, - {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"}, -] - -[[package]] -name = "frozenlist" -version = "1.3.3" -description = "A list-like structure which implements collections.abc.MutableSequence" -optional = false -python-versions = ">=3.7" -files = [ - {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff8bf625fe85e119553b5383ba0fb6aa3d0ec2ae980295aaefa552374926b3f4"}, - {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dfbac4c2dfcc082fcf8d942d1e49b6aa0766c19d3358bd86e2000bf0fa4a9cf0"}, - {file = "frozenlist-1.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b1c63e8d377d039ac769cd0926558bb7068a1f7abb0f003e3717ee003ad85530"}, - {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fdfc24dcfce5b48109867c13b4cb15e4660e7bd7661741a391f821f23dfdca7"}, - {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c926450857408e42f0bbc295e84395722ce74bae69a3b2aa2a65fe22cb14b99"}, - {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1841e200fdafc3d51f974d9d377c079a0694a8f06de2e67b48150328d66d5483"}, - {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f470c92737afa7d4c3aacc001e335062d582053d4dbe73cda126f2d7031068dd"}, - {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:783263a4eaad7c49983fe4b2e7b53fa9770c136c270d2d4bbb6d2192bf4d9caf"}, - {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:924620eef691990dfb56dc4709f280f40baee568c794b5c1885800c3ecc69816"}, - {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ae4dc05c465a08a866b7a1baf360747078b362e6a6dbeb0c57f234db0ef88ae0"}, - {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:bed331fe18f58d844d39ceb398b77d6ac0b010d571cba8267c2e7165806b00ce"}, - {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:02c9ac843e3390826a265e331105efeab489ffaf4dd86384595ee8ce6d35ae7f"}, - {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9545a33965d0d377b0bc823dcabf26980e77f1b6a7caa368a365a9497fb09420"}, - {file = "frozenlist-1.3.3-cp310-cp310-win32.whl", hash = "sha256:d5cd3ab21acbdb414bb6c31958d7b06b85eeb40f66463c264a9b343a4e238642"}, - {file = "frozenlist-1.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:b756072364347cb6aa5b60f9bc18e94b2f79632de3b0190253ad770c5df17db1"}, - {file = "frozenlist-1.3.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b4395e2f8d83fbe0c627b2b696acce67868793d7d9750e90e39592b3626691b7"}, - {file = "frozenlist-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14143ae966a6229350021384870458e4777d1eae4c28d1a7aa47f24d030e6678"}, - {file = "frozenlist-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5d8860749e813a6f65bad8285a0520607c9500caa23fea6ee407e63debcdbef6"}, - {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23d16d9f477bb55b6154654e0e74557040575d9d19fe78a161bd33d7d76808e8"}, - {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb82dbba47a8318e75f679690190c10a5e1f447fbf9df41cbc4c3afd726d88cb"}, - {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9309869032abb23d196cb4e4db574232abe8b8be1339026f489eeb34a4acfd91"}, - {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a97b4fe50b5890d36300820abd305694cb865ddb7885049587a5678215782a6b"}, - {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c188512b43542b1e91cadc3c6c915a82a5eb95929134faf7fd109f14f9892ce4"}, - {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:303e04d422e9b911a09ad499b0368dc551e8c3cd15293c99160c7f1f07b59a48"}, - {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:0771aed7f596c7d73444c847a1c16288937ef988dc04fb9f7be4b2aa91db609d"}, - {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:66080ec69883597e4d026f2f71a231a1ee9887835902dbe6b6467d5a89216cf6"}, - {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:41fe21dc74ad3a779c3d73a2786bdf622ea81234bdd4faf90b8b03cad0c2c0b4"}, - {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f20380df709d91525e4bee04746ba612a4df0972c1b8f8e1e8af997e678c7b81"}, - {file = "frozenlist-1.3.3-cp311-cp311-win32.whl", hash = "sha256:f30f1928162e189091cf4d9da2eac617bfe78ef907a761614ff577ef4edfb3c8"}, - {file = "frozenlist-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:a6394d7dadd3cfe3f4b3b186e54d5d8504d44f2d58dcc89d693698e8b7132b32"}, - {file = "frozenlist-1.3.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8df3de3a9ab8325f94f646609a66cbeeede263910c5c0de0101079ad541af332"}, - {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0693c609e9742c66ba4870bcee1ad5ff35462d5ffec18710b4ac89337ff16e27"}, - {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd4210baef299717db0a600d7a3cac81d46ef0e007f88c9335db79f8979c0d3d"}, - {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:394c9c242113bfb4b9aa36e2b80a05ffa163a30691c7b5a29eba82e937895d5e"}, - {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6327eb8e419f7d9c38f333cde41b9ae348bec26d840927332f17e887a8dcb70d"}, - {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e24900aa13212e75e5b366cb9065e78bbf3893d4baab6052d1aca10d46d944c"}, - {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3843f84a6c465a36559161e6c59dce2f2ac10943040c2fd021cfb70d58c4ad56"}, - {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:84610c1502b2461255b4c9b7d5e9c48052601a8957cd0aea6ec7a7a1e1fb9420"}, - {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:c21b9aa40e08e4f63a2f92ff3748e6b6c84d717d033c7b3438dd3123ee18f70e"}, - {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:efce6ae830831ab6a22b9b4091d411698145cb9b8fc869e1397ccf4b4b6455cb"}, - {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:40de71985e9042ca00b7953c4f41eabc3dc514a2d1ff534027f091bc74416401"}, - {file = "frozenlist-1.3.3-cp37-cp37m-win32.whl", hash = "sha256:180c00c66bde6146a860cbb81b54ee0df350d2daf13ca85b275123bbf85de18a"}, - {file = "frozenlist-1.3.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9bbbcedd75acdfecf2159663b87f1bb5cfc80e7cd99f7ddd9d66eb98b14a8411"}, - {file = "frozenlist-1.3.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:034a5c08d36649591be1cbb10e09da9f531034acfe29275fc5454a3b101ce41a"}, - {file = "frozenlist-1.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba64dc2b3b7b158c6660d49cdb1d872d1d0bf4e42043ad8d5006099479a194e5"}, - {file = "frozenlist-1.3.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:47df36a9fe24054b950bbc2db630d508cca3aa27ed0566c0baf661225e52c18e"}, - {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:008a054b75d77c995ea26629ab3a0c0d7281341f2fa7e1e85fa6153ae29ae99c"}, - {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:841ea19b43d438a80b4de62ac6ab21cfe6827bb8a9dc62b896acc88eaf9cecba"}, - {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e235688f42b36be2b6b06fc37ac2126a73b75fb8d6bc66dd632aa35286238703"}, - {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca713d4af15bae6e5d79b15c10c8522859a9a89d3b361a50b817c98c2fb402a2"}, - {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ac5995f2b408017b0be26d4a1d7c61bce106ff3d9e3324374d66b5964325448"}, - {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4ae8135b11652b08a8baf07631d3ebfe65a4c87909dbef5fa0cdde440444ee4"}, - {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4ea42116ceb6bb16dbb7d526e242cb6747b08b7710d9782aa3d6732bd8d27649"}, - {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:810860bb4bdce7557bc0febb84bbd88198b9dbc2022d8eebe5b3590b2ad6c842"}, - {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ee78feb9d293c323b59a6f2dd441b63339a30edf35abcb51187d2fc26e696d13"}, - {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0af2e7c87d35b38732e810befb9d797a99279cbb85374d42ea61c1e9d23094b3"}, - {file = "frozenlist-1.3.3-cp38-cp38-win32.whl", hash = "sha256:899c5e1928eec13fd6f6d8dc51be23f0d09c5281e40d9cf4273d188d9feeaf9b"}, - {file = "frozenlist-1.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:7f44e24fa70f6fbc74aeec3e971f60a14dde85da364aa87f15d1be94ae75aeef"}, - {file = "frozenlist-1.3.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2b07ae0c1edaa0a36339ec6cce700f51b14a3fc6545fdd32930d2c83917332cf"}, - {file = "frozenlist-1.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ebb86518203e12e96af765ee89034a1dbb0c3c65052d1b0c19bbbd6af8a145e1"}, - {file = "frozenlist-1.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5cf820485f1b4c91e0417ea0afd41ce5cf5965011b3c22c400f6d144296ccbc0"}, - {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c11e43016b9024240212d2a65043b70ed8dfd3b52678a1271972702d990ac6d"}, - {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8fa3c6e3305aa1146b59a09b32b2e04074945ffcfb2f0931836d103a2c38f936"}, - {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:352bd4c8c72d508778cf05ab491f6ef36149f4d0cb3c56b1b4302852255d05d5"}, - {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65a5e4d3aa679610ac6e3569e865425b23b372277f89b5ef06cf2cdaf1ebf22b"}, - {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e2c1185858d7e10ff045c496bbf90ae752c28b365fef2c09cf0fa309291669"}, - {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f163d2fd041c630fed01bc48d28c3ed4a3b003c00acd396900e11ee5316b56bb"}, - {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:05cdb16d09a0832eedf770cb7bd1fe57d8cf4eaf5aced29c4e41e3f20b30a784"}, - {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:8bae29d60768bfa8fb92244b74502b18fae55a80eac13c88eb0b496d4268fd2d"}, - {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:eedab4c310c0299961ac285591acd53dc6723a1ebd90a57207c71f6e0c2153ab"}, - {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3bbdf44855ed8f0fbcd102ef05ec3012d6a4fd7c7562403f76ce6a52aeffb2b1"}, - {file = "frozenlist-1.3.3-cp39-cp39-win32.whl", hash = "sha256:efa568b885bca461f7c7b9e032655c0c143d305bf01c30caf6db2854a4532b38"}, - {file = "frozenlist-1.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:cfe33efc9cb900a4c46f91a5ceba26d6df370ffddd9ca386eb1d4f0ad97b9ea9"}, - {file = "frozenlist-1.3.3.tar.gz", hash = "sha256:58bcc55721e8a90b88332d6cd441261ebb22342e238296bb330968952fbb3a6a"}, + {file = "charset-normalizer-3.3.0.tar.gz", hash = "sha256:63563193aec44bce707e0c5ca64ff69fa72ed7cf34ce6e11d5127555756fd2f6"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:effe5406c9bd748a871dbcaf3ac69167c38d72db8c9baf3ff954c344f31c4cbe"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4162918ef3098851fcd8a628bf9b6a98d10c380725df9e04caf5ca6dd48c847a"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0570d21da019941634a531444364f2482e8db0b3425fcd5ac0c36565a64142c8"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5707a746c6083a3a74b46b3a631d78d129edab06195a92a8ece755aac25a3f3d"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:278c296c6f96fa686d74eb449ea1697f3c03dc28b75f873b65b5201806346a69"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a4b71f4d1765639372a3b32d2638197f5cd5221b19531f9245fcc9ee62d38f56"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5969baeaea61c97efa706b9b107dcba02784b1601c74ac84f2a532ea079403e"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3f93dab657839dfa61025056606600a11d0b696d79386f974e459a3fbc568ec"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:db756e48f9c5c607b5e33dd36b1d5872d0422e960145b08ab0ec7fd420e9d649"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:232ac332403e37e4a03d209a3f92ed9071f7d3dbda70e2a5e9cff1c4ba9f0678"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e5c1502d4ace69a179305abb3f0bb6141cbe4714bc9b31d427329a95acfc8bdd"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:2502dd2a736c879c0f0d3e2161e74d9907231e25d35794584b1ca5284e43f596"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23e8565ab7ff33218530bc817922fae827420f143479b753104ab801145b1d5b"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-win32.whl", hash = "sha256:1872d01ac8c618a8da634e232f24793883d6e456a66593135aeafe3784b0848d"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:557b21a44ceac6c6b9773bc65aa1b4cc3e248a5ad2f5b914b91579a32e22204d"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d7eff0f27edc5afa9e405f7165f85a6d782d308f3b6b9d96016c010597958e63"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a685067d05e46641d5d1623d7c7fdf15a357546cbb2f71b0ebde91b175ffc3e"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0d3d5b7db9ed8a2b11a774db2bbea7ba1884430a205dbd54a32d61d7c2a190fa"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2935ffc78db9645cb2086c2f8f4cfd23d9b73cc0dc80334bc30aac6f03f68f8c"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fe359b2e3a7729010060fbca442ca225280c16e923b37db0e955ac2a2b72a05"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380c4bde80bce25c6e4f77b19386f5ec9db230df9f2f2ac1e5ad7af2caa70459"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0d1e3732768fecb052d90d62b220af62ead5748ac51ef61e7b32c266cac9293"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b2919306936ac6efb3aed1fbf81039f7087ddadb3160882a57ee2ff74fd2382"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f8888e31e3a85943743f8fc15e71536bda1c81d5aa36d014a3c0c44481d7db6e"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:82eb849f085624f6a607538ee7b83a6d8126df6d2f7d3b319cb837b289123078"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7b8b8bf1189b3ba9b8de5c8db4d541b406611a71a955bbbd7385bbc45fcb786c"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5adf257bd58c1b8632046bbe43ee38c04e1038e9d37de9c57a94d6bd6ce5da34"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c350354efb159b8767a6244c166f66e67506e06c8924ed74669b2c70bc8735b1"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-win32.whl", hash = "sha256:02af06682e3590ab952599fbadac535ede5d60d78848e555aa58d0c0abbde786"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:86d1f65ac145e2c9ed71d8ffb1905e9bba3a91ae29ba55b4c46ae6fc31d7c0d4"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:3b447982ad46348c02cb90d230b75ac34e9886273df3a93eec0539308a6296d7"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:abf0d9f45ea5fb95051c8bfe43cb40cda383772f7e5023a83cc481ca2604d74e"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b09719a17a2301178fac4470d54b1680b18a5048b481cb8890e1ef820cb80455"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3d9b48ee6e3967b7901c052b670c7dda6deb812c309439adaffdec55c6d7b78"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:edfe077ab09442d4ef3c52cb1f9dab89bff02f4524afc0acf2d46be17dc479f5"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3debd1150027933210c2fc321527c2299118aa929c2f5a0a80ab6953e3bd1908"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86f63face3a527284f7bb8a9d4f78988e3c06823f7bea2bd6f0e0e9298ca0403"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24817cb02cbef7cd499f7c9a2735286b4782bd47a5b3516a0e84c50eab44b98e"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c71f16da1ed8949774ef79f4a0260d28b83b3a50c6576f8f4f0288d109777989"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9cf3126b85822c4e53aa28c7ec9869b924d6fcfb76e77a45c44b83d91afd74f9"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:b3b2316b25644b23b54a6f6401074cebcecd1244c0b8e80111c9a3f1c8e83d65"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:03680bb39035fbcffe828eae9c3f8afc0428c91d38e7d61aa992ef7a59fb120e"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cc152c5dd831641e995764f9f0b6589519f6f5123258ccaca8c6d34572fefa8"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-win32.whl", hash = "sha256:b8f3307af845803fb0b060ab76cf6dd3a13adc15b6b451f54281d25911eb92df"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:8eaf82f0eccd1505cf39a45a6bd0a8cf1c70dcfc30dba338207a969d91b965c0"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dc45229747b67ffc441b3de2f3ae5e62877a282ea828a5bdb67883c4ee4a8810"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f4a0033ce9a76e391542c182f0d48d084855b5fcba5010f707c8e8c34663d77"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ada214c6fa40f8d800e575de6b91a40d0548139e5dc457d2ebb61470abf50186"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1121de0e9d6e6ca08289583d7491e7fcb18a439305b34a30b20d8215922d43c"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1063da2c85b95f2d1a430f1c33b55c9c17ffaf5e612e10aeaad641c55a9e2b9d"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70f1d09c0d7748b73290b29219e854b3207aea922f839437870d8cc2168e31cc"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:250c9eb0f4600361dd80d46112213dff2286231d92d3e52af1e5a6083d10cad9"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:750b446b2ffce1739e8578576092179160f6d26bd5e23eb1789c4d64d5af7dc7"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:fc52b79d83a3fe3a360902d3f5d79073a993597d48114c29485e9431092905d8"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:588245972aca710b5b68802c8cad9edaa98589b1b42ad2b53accd6910dad3545"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e39c7eb31e3f5b1f88caff88bcff1b7f8334975b46f6ac6e9fc725d829bc35d4"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-win32.whl", hash = "sha256:abecce40dfebbfa6abf8e324e1860092eeca6f7375c8c4e655a8afb61af58f2c"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:24a91a981f185721542a0b7c92e9054b7ab4fea0508a795846bc5b0abf8118d4"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:67b8cc9574bb518ec76dc8e705d4c39ae78bb96237cb533edac149352c1f39fe"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac71b2977fb90c35d41c9453116e283fac47bb9096ad917b8819ca8b943abecd"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3ae38d325b512f63f8da31f826e6cb6c367336f95e418137286ba362925c877e"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:542da1178c1c6af8873e143910e2269add130a299c9106eef2594e15dae5e482"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30a85aed0b864ac88309b7d94be09f6046c834ef60762a8833b660139cfbad13"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aae32c93e0f64469f74ccc730a7cb21c7610af3a775157e50bbd38f816536b38"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b26ddf78d57f1d143bdf32e820fd8935d36abe8a25eb9ec0b5a71c82eb3895"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f5d10bae5d78e4551b7be7a9b29643a95aded9d0f602aa2ba584f0388e7a557"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:249c6470a2b60935bafd1d1d13cd613f8cd8388d53461c67397ee6a0f5dce741"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c5a74c359b2d47d26cdbbc7845e9662d6b08a1e915eb015d044729e92e7050b7"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:b5bcf60a228acae568e9911f410f9d9e0d43197d030ae5799e20dca8df588287"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:187d18082694a29005ba2944c882344b6748d5be69e3a89bf3cc9d878e548d5a"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:81bf654678e575403736b85ba3a7867e31c2c30a69bc57fe88e3ace52fb17b89"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-win32.whl", hash = "sha256:85a32721ddde63c9df9ebb0d2045b9691d9750cb139c161c80e500d210f5e26e"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:468d2a840567b13a590e67dd276c570f8de00ed767ecc611994c301d0f8c014f"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e0fc42822278451bc13a2e8626cf2218ba570f27856b536e00cfa53099724828"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:09c77f964f351a7369cc343911e0df63e762e42bac24cd7d18525961c81754f4"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:12ebea541c44fdc88ccb794a13fe861cc5e35d64ed689513a5c03d05b53b7c82"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:805dfea4ca10411a5296bcc75638017215a93ffb584c9e344731eef0dcfb026a"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:96c2b49eb6a72c0e4991d62406e365d87067ca14c1a729a870d22354e6f68115"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aaf7b34c5bc56b38c931a54f7952f1ff0ae77a2e82496583b247f7c969eb1479"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:619d1c96099be5823db34fe89e2582b336b5b074a7f47f819d6b3a57ff7bdb86"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0ac5e7015a5920cfce654c06618ec40c33e12801711da6b4258af59a8eff00a"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:93aa7eef6ee71c629b51ef873991d6911b906d7312c6e8e99790c0f33c576f89"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7966951325782121e67c81299a031f4c115615e68046f79b85856b86ebffc4cd"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:02673e456dc5ab13659f85196c534dc596d4ef260e4d86e856c3b2773ce09843"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:c2af80fb58f0f24b3f3adcb9148e6203fa67dd3f61c4af146ecad033024dde43"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:153e7b6e724761741e0974fc4dcd406d35ba70b92bfe3fedcb497226c93b9da7"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-win32.whl", hash = "sha256:d47ecf253780c90ee181d4d871cd655a789da937454045b17b5798da9393901a"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:d97d85fa63f315a8bdaba2af9a6a686e0eceab77b3089af45133252618e70884"}, + {file = "charset_normalizer-3.3.0-py3-none-any.whl", hash = "sha256:e46cd37076971c1040fc8c41273a8b3e2c624ce4f2be3f5dfcb7a430c1d3acc2"}, ] [[package]] @@ -360,134 +163,39 @@ files = [ ] [[package]] -name = "importlib-metadata" -version = "6.7.0" -description = "Read metadata from Python packages" +name = "jmespath" +version = "1.0.1" +description = "JSON Matching Expressions" optional = false python-versions = ">=3.7" files = [ - {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"}, - {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"}, -] - -[package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} -zipp = ">=0.5" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] - -[[package]] -name = "multidict" -version = "6.0.4" -description = "multidict implementation" -optional = false -python-versions = ">=3.7" -files = [ - {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"}, - {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"}, - {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"}, - {file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"}, - {file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"}, - {file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"}, - {file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"}, - {file = "multidict-6.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d"}, - {file = "multidict-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775"}, - {file = "multidict-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"}, - {file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"}, - {file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"}, - {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"}, - {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, - {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, + {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, + {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, ] [[package]] name = "opensearch-py" -version = "2.2.0" +version = "2.3.2" description = "Python client for OpenSearch" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" -files = [] -develop = false +files = [ + {file = "opensearch-py-2.3.2.tar.gz", hash = "sha256:96e470b55107fd5bfd873722dc9808c333360eacfa174341f5cc2d021aa30448"}, + {file = "opensearch_py-2.3.2-py2.py3-none-any.whl", hash = "sha256:b1d6607380c8f19d90c142470939d051f0bac96069ce0ac25970b3c39c431f8b"}, +] [package.dependencies] -aiohttp = {version = ">=3,<4", optional = true, markers = "extra == \"async\""} certifi = ">=2022.12.07" python-dateutil = "*" requests = ">=2.4.0,<3.0.0" six = "*" -urllib3 = ">=1.21.1,<2" +urllib3 = ">=1.26.9" [package.extras] async = ["aiohttp (>=3,<4)"] -develop = ["black", "botocore", "coverage (<7.0.0)", "jinja2", "mock", "myst_parser", "pytest (>=3.0.0)", "pytest-cov", "pytest-mock (<4.0.0)", "pytz", "pyyaml", "requests (>=2.0.0,<3.0.0)", "sphinx", "sphinx_copybutton", "sphinx_rtd_theme"] -docs = ["myst_parser", "sphinx", "sphinx_copybutton", "sphinx_rtd_theme"] -kerberos = ["requests_kerberos"] - -[package.source] -type = "directory" -url = ".." +develop = ["black", "botocore", "coverage (<7.0.0)", "jinja2", "mock", "myst-parser", "pytest (>=3.0.0)", "pytest-cov", "pytest-mock (<4.0.0)", "pytz", "pyyaml", "requests (>=2.0.0,<3.0.0)", "sphinx", "sphinx-copybutton", "sphinx-rtd-theme"] +docs = ["myst-parser", "sphinx", "sphinx-copybutton", "sphinx-rtd-theme"] +kerberos = ["requests-kerberos"] [[package]] name = "python-dateutil" @@ -524,6 +232,23 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "s3transfer" +version = "0.7.0" +description = "An Amazon S3 Transfer Manager" +optional = false +python-versions = ">= 3.7" +files = [ + {file = "s3transfer-0.7.0-py3-none-any.whl", hash = "sha256:10d6923c6359175f264811ef4bf6161a3156ce8e350e705396a7557d6293c33a"}, + {file = "s3transfer-0.7.0.tar.gz", hash = "sha256:fd3889a66f5fe17299fe75b82eae6cf722554edca744ca5d5fe308b104883d2e"}, +] + +[package.dependencies] +botocore = ">=1.12.36,<2.0a.0" + +[package.extras] +crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"] + [[package]] name = "six" version = "1.16.0" @@ -535,137 +260,40 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] -[[package]] -name = "typing-extensions" -version = "4.7.1" -description = "Backported and Experimental Type Hints for Python 3.7+" -optional = false -python-versions = ">=3.7" -files = [ - {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, - {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, -] - [[package]] name = "urllib3" -version = "1.26.16" +version = "1.26.18" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ - {file = "urllib3-1.26.16-py2.py3-none-any.whl", hash = "sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f"}, - {file = "urllib3-1.26.16.tar.gz", hash = "sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14"}, + {file = "urllib3-1.26.18-py2.py3-none-any.whl", hash = "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07"}, + {file = "urllib3-1.26.18.tar.gz", hash = "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] -name = "yarl" -version = "1.9.2" -description = "Yet another URL library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c2ad583743d16ddbdf6bb14b5cd76bf43b0d0006e918809d5d4ddf7bde8dd82"}, - {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:82aa6264b36c50acfb2424ad5ca537a2060ab6de158a5bd2a72a032cc75b9eb8"}, - {file = "yarl-1.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c0c77533b5ed4bcc38e943178ccae29b9bcf48ffd1063f5821192f23a1bd27b9"}, - {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560"}, - {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bf345c3a4f5ba7f766430f97f9cc1320786f19584acc7086491f45524a551ac"}, - {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a96c19c52ff442a808c105901d0bdfd2e28575b3d5f82e2f5fd67e20dc5f4ea"}, - {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:891c0e3ec5ec881541f6c5113d8df0315ce5440e244a716b95f2525b7b9f3608"}, - {file = "yarl-1.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c3a53ba34a636a256d767c086ceb111358876e1fb6b50dfc4d3f4951d40133d5"}, - {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:566185e8ebc0898b11f8026447eacd02e46226716229cea8db37496c8cdd26e0"}, - {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2b0738fb871812722a0ac2154be1f049c6223b9f6f22eec352996b69775b36d4"}, - {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:32f1d071b3f362c80f1a7d322bfd7b2d11e33d2adf395cc1dd4df36c9c243095"}, - {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3"}, - {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:56ff08ab5df8429901ebdc5d15941b59f6253393cb5da07b4170beefcf1b2528"}, - {file = "yarl-1.9.2-cp310-cp310-win32.whl", hash = "sha256:8ea48e0a2f931064469bdabca50c2f578b565fc446f302a79ba6cc0ee7f384d3"}, - {file = "yarl-1.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:50f33040f3836e912ed16d212f6cc1efb3231a8a60526a407aeb66c1c1956dde"}, - {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:646d663eb2232d7909e6601f1a9107e66f9791f290a1b3dc7057818fe44fc2b6"}, - {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aff634b15beff8902d1f918012fc2a42e0dbae6f469fce134c8a0dc51ca423bb"}, - {file = "yarl-1.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a83503934c6273806aed765035716216cc9ab4e0364f7f066227e1aaea90b8d0"}, - {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b25322201585c69abc7b0e89e72790469f7dad90d26754717f3310bfe30331c2"}, - {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22a94666751778629f1ec4280b08eb11815783c63f52092a5953faf73be24191"}, - {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ec53a0ea2a80c5cd1ab397925f94bff59222aa3cf9c6da938ce05c9ec20428d"}, - {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:159d81f22d7a43e6eabc36d7194cb53f2f15f498dbbfa8edc8a3239350f59fe7"}, - {file = "yarl-1.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:832b7e711027c114d79dffb92576acd1bd2decc467dec60e1cac96912602d0e6"}, - {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:95d2ecefbcf4e744ea952d073c6922e72ee650ffc79028eb1e320e732898d7e8"}, - {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9"}, - {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:783185c75c12a017cc345015ea359cc801c3b29a2966c2655cd12b233bf5a2be"}, - {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:b8cc1863402472f16c600e3e93d542b7e7542a540f95c30afd472e8e549fc3f7"}, - {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:822b30a0f22e588b32d3120f6d41e4ed021806418b4c9f0bc3048b8c8cb3f92a"}, - {file = "yarl-1.9.2-cp311-cp311-win32.whl", hash = "sha256:a60347f234c2212a9f0361955007fcf4033a75bf600a33c88a0a8e91af77c0e8"}, - {file = "yarl-1.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:be6b3fdec5c62f2a67cb3f8c6dbf56bbf3f61c0f046f84645cd1ca73532ea051"}, - {file = "yarl-1.9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38a3928ae37558bc1b559f67410df446d1fbfa87318b124bf5032c31e3447b74"}, - {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac9bb4c5ce3975aeac288cfcb5061ce60e0d14d92209e780c93954076c7c4367"}, - {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3da8a678ca8b96c8606bbb8bfacd99a12ad5dd288bc6f7979baddd62f71c63ef"}, - {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13414591ff516e04fcdee8dc051c13fd3db13b673c7a4cb1350e6b2ad9639ad3"}, - {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf74d08542c3a9ea97bb8f343d4fcbd4d8f91bba5ec9d5d7f792dbe727f88938"}, - {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e7221580dc1db478464cfeef9b03b95c5852cc22894e418562997df0d074ccc"}, - {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:494053246b119b041960ddcd20fd76224149cfea8ed8777b687358727911dd33"}, - {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:52a25809fcbecfc63ac9ba0c0fb586f90837f5425edfd1ec9f3372b119585e45"}, - {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:e65610c5792870d45d7b68c677681376fcf9cc1c289f23e8e8b39c1485384185"}, - {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:1b1bba902cba32cdec51fca038fd53f8beee88b77efc373968d1ed021024cc04"}, - {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:662e6016409828ee910f5d9602a2729a8a57d74b163c89a837de3fea050c7582"}, - {file = "yarl-1.9.2-cp37-cp37m-win32.whl", hash = "sha256:f364d3480bffd3aa566e886587eaca7c8c04d74f6e8933f3f2c996b7f09bee1b"}, - {file = "yarl-1.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6a5883464143ab3ae9ba68daae8e7c5c95b969462bbe42e2464d60e7e2698368"}, - {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5610f80cf43b6202e2c33ba3ec2ee0a2884f8f423c8f4f62906731d876ef4fac"}, - {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b9a4e67ad7b646cd6f0938c7ebfd60e481b7410f574c560e455e938d2da8e0f4"}, - {file = "yarl-1.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:83fcc480d7549ccebe9415d96d9263e2d4226798c37ebd18c930fce43dfb9574"}, - {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fcd436ea16fee7d4207c045b1e340020e58a2597301cfbcfdbe5abd2356c2fb"}, - {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84e0b1599334b1e1478db01b756e55937d4614f8654311eb26012091be109d59"}, - {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3458a24e4ea3fd8930e934c129b676c27452e4ebda80fbe47b56d8c6c7a63a9e"}, - {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:838162460b3a08987546e881a2bfa573960bb559dfa739e7800ceeec92e64417"}, - {file = "yarl-1.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4e2d08f07a3d7d3e12549052eb5ad3eab1c349c53ac51c209a0e5991bbada78"}, - {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:de119f56f3c5f0e2fb4dee508531a32b069a5f2c6e827b272d1e0ff5ac040333"}, - {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:149ddea5abf329752ea5051b61bd6c1d979e13fbf122d3a1f9f0c8be6cb6f63c"}, - {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:674ca19cbee4a82c9f54e0d1eee28116e63bc6fd1e96c43031d11cbab8b2afd5"}, - {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:9b3152f2f5677b997ae6c804b73da05a39daa6a9e85a512e0e6823d81cdad7cc"}, - {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5415d5a4b080dc9612b1b63cba008db84e908b95848369aa1da3686ae27b6d2b"}, - {file = "yarl-1.9.2-cp38-cp38-win32.whl", hash = "sha256:f7a3d8146575e08c29ed1cd287068e6d02f1c7bdff8970db96683b9591b86ee7"}, - {file = "yarl-1.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:63c48f6cef34e6319a74c727376e95626f84ea091f92c0250a98e53e62c77c72"}, - {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75df5ef94c3fdc393c6b19d80e6ef1ecc9ae2f4263c09cacb178d871c02a5ba9"}, - {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c027a6e96ef77d401d8d5a5c8d6bc478e8042f1e448272e8d9752cb0aff8b5c8"}, - {file = "yarl-1.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3b078dbe227f79be488ffcfc7a9edb3409d018e0952cf13f15fd6512847f3f7"}, - {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59723a029760079b7d991a401386390c4be5bfec1e7dd83e25a6a0881859e716"}, - {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b03917871bf859a81ccb180c9a2e6c1e04d2f6a51d953e6a5cdd70c93d4e5a2a"}, - {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1012fa63eb6c032f3ce5d2171c267992ae0c00b9e164efe4d73db818465fac3"}, - {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a74dcbfe780e62f4b5a062714576f16c2f3493a0394e555ab141bf0d746bb955"}, - {file = "yarl-1.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c56986609b057b4839968ba901944af91b8e92f1725d1a2d77cbac6972b9ed1"}, - {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2c315df3293cd521033533d242d15eab26583360b58f7ee5d9565f15fee1bef4"}, - {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b7232f8dfbd225d57340e441d8caf8652a6acd06b389ea2d3222b8bc89cbfca6"}, - {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:53338749febd28935d55b41bf0bcc79d634881195a39f6b2f767870b72514caf"}, - {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:066c163aec9d3d073dc9ffe5dd3ad05069bcb03fcaab8d221290ba99f9f69ee3"}, - {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8288d7cd28f8119b07dd49b7230d6b4562f9b61ee9a4ab02221060d21136be80"}, - {file = "yarl-1.9.2-cp39-cp39-win32.whl", hash = "sha256:b124e2a6d223b65ba8768d5706d103280914d61f5cae3afbc50fc3dfcc016623"}, - {file = "yarl-1.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:61016e7d582bc46a5378ffdd02cd0314fb8ba52f40f9cf4d9a5e7dbef88dee18"}, - {file = "yarl-1.9.2.tar.gz", hash = "sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571"}, -] - -[package.dependencies] -idna = ">=2.0" -multidict = ">=4.0" -typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} - -[[package]] -name = "zipp" -version = "3.15.0" -description = "Backport of pathlib-compatible object wrapper for zip files" +name = "urllib3" +version = "2.0.7" +description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.7" files = [ - {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, - {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, + {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, + {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "daae1667da61b85f9bbc2e5c484721c424594da7cb08fe9cf85c08d6731bcc52" +content-hash = "1309989011bed3cb46e36fc451b65f040ef9fe9cecbe3f3706be240d4ea6d52e" diff --git a/samples/pyproject.toml b/samples/pyproject.toml index 380e2c9a..8a89367f 100644 --- a/samples/pyproject.toml +++ b/samples/pyproject.toml @@ -8,7 +8,8 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.7" -opensearch-py = { path = "../", extras=["async"] } +opensearch-py = { path = "../", extras=["async"], develop = true } +boto3 = "^1.28" [build-system] requires = ["poetry-core"] diff --git a/samples/sigv4/README.md b/samples/sigv4/README.md new file mode 100644 index 00000000..cb5d0eae --- /dev/null +++ b/samples/sigv4/README.md @@ -0,0 +1,22 @@ +## AWS SigV4 Samples + +Create an OpenSearch domain in (AWS) which support IAM based AuthN/AuthZ. + +``` +export AWS_ACCESS_KEY_ID= +export AWS_SECRET_ACCESS_KEY= +export AWS_SESSION_TOKEN= +export AWS_REGION=us-west-2 + +export SERVICE=es # use "aoss" for OpenSearch Serverless. +export ENDPOINT=https://....us-west-2.es.amazonaws.com + +poetry run sigv4/requests-hello.py +``` + +This will output the version of OpenSearch and a search result. + +``` +opensearch: 2.3.0 +{'director': 'Bennett Miller', 'title': 'Moneyball', 'year': 2011} +``` diff --git a/samples/sigv4/requests-hello.py b/samples/sigv4/requests-hello.py new file mode 100644 index 00000000..c362db86 --- /dev/null +++ b/samples/sigv4/requests-hello.py @@ -0,0 +1,69 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# The OpenSearch Contributors require contributions made to +# this file be licensed under the Apache-2.0 license or a +# compatible open source license. +# +# Modifications Copyright OpenSearch Contributors. See +# GitHub history for details. + +import logging + +from os import environ +from time import sleep +from urllib.parse import urlparse + +from boto3 import Session +from opensearchpy import AWSV4SignerAuth, OpenSearch, RequestsHttpConnection + +# verbose logging +logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.INFO) + +# cluster endpoint, for example: my-test-domain.us-east-1.es.amazonaws.com +url = urlparse(environ['ENDPOINT']) +region = environ.get('AWS_REGION', 'us-east-1') +service = environ.get('SERVICE', 'es') + +credentials = Session().get_credentials() + +auth = AWSV4SignerAuth(credentials, region, service) + +client = OpenSearch( + hosts=[{ + 'host': url.netloc, + 'port': url.port or 443 + }], + http_auth=auth, + use_ssl=True, + verify_certs=True, + connection_class=RequestsHttpConnection, + timeout=30 +) + +# TODO: remove when OpenSearch Serverless adds support for / +if service == 'es': + info = client.info() + print(f"{info['version']['distribution']}: {info['version']['number']}") + +# create an index +index = 'movies' +client.indices.create(index=index) + +try: + # index data + document = {'director': 'Bennett Miller', 'title': 'Moneyball', 'year': 2011} + client.index(index=index, body=document, id='1') + + # wait for the document to index + sleep(1) + + # search for the document + results = client.search(body={'query': {'match': {'director': 'miller'}}}) + for hit in results['hits']['hits']: + print(hit['_source']) + + # delete the document + client.delete(index=index, id='1') +finally: + # delete the index + client.indices.delete(index=index) \ No newline at end of file diff --git a/samples/sigv4/urllib3-hello.py b/samples/sigv4/urllib3-hello.py new file mode 100644 index 00000000..09670e0d --- /dev/null +++ b/samples/sigv4/urllib3-hello.py @@ -0,0 +1,69 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# The OpenSearch Contributors require contributions made to +# this file be licensed under the Apache-2.0 license or a +# compatible open source license. +# +# Modifications Copyright OpenSearch Contributors. See +# GitHub history for details. + +import logging + +from os import environ +from time import sleep +from urllib.parse import urlparse + +from boto3 import Session +from opensearchpy import UrlLib3AWSV4SignerAuth, OpenSearch, Urllib3HttpConnection + +# verbose logging +logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.INFO) + +# cluster endpoint, for example: my-test-domain.us-east-1.es.amazonaws.com +url = urlparse(environ['ENDPOINT']) +region = environ.get('AWS_REGION', 'us-east-1') +service = environ.get('SERVICE', 'es') + +credentials = Session().get_credentials() + +auth = UrlLib3AWSV4SignerAuth(credentials, region, service) + +client = OpenSearch( + hosts=[{ + 'host': url.netloc, + 'port': url.port or 443 + }], + http_auth=auth, + use_ssl=True, + verify_certs=True, + connection_class=Urllib3HttpConnection, + timeout=30 +) + +# TODO: remove when OpenSearch Serverless adds support for / +if service == 'es': + info = client.info() + print(f"{info['version']['distribution']}: {info['version']['number']}") + +# create an index +index = 'movies' +client.indices.create(index=index) + +try: + # index data + document = {'director': 'Bennett Miller', 'title': 'Moneyball', 'year': 2011} + client.index(index=index, body=document, id='1') + + # wait for the document to index + sleep(1) + + # search for the document + results = client.search(body={'query': {'match': {'director': 'miller'}}}) + for hit in results['hits']['hits']: + print(hit['_source']) + + # delete the document + client.delete(index=index, id='1') +finally: + # delete the index + client.indices.delete(index=index) \ No newline at end of file From 6500c2bd5837781d51a6ed5834a873e6819cfc27 Mon Sep 17 00:00:00 2001 From: dblock Date: Fri, 20 Oct 2023 17:30:34 -0400 Subject: [PATCH 04/13] Updated CHANGELOG. Signed-off-by: dblock --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96a4b1ac..c162fc62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Added `pool_maxsize` for `Urllib3HttpConnection` ([#535](https://github.com/opensearch-project/opensearch-py/pull/535)) - Added benchmarks ([#537](https://github.com/opensearch-project/opensearch-py/pull/537)) - Added guide on making raw JSON REST requests ([#542](https://github.com/opensearch-project/opensearch-py/pull/542)) +- Added support for AWS SigV4 for urllib3 ([#547](https://github.com/opensearch-project/opensearch-py/pull/547)) ### Changed - Generate `tasks` client from API specs ([#508](https://github.com/opensearch-project/opensearch-py/pull/508)) - Generate `ingest` client from API specs ([#513](https://github.com/opensearch-project/opensearch-py/pull/513)) From f8f2b6235027c77fa63cdea519f6e2b2d484642f Mon Sep 17 00:00:00 2001 From: dblock Date: Fri, 20 Oct 2023 17:39:30 -0400 Subject: [PATCH 05/13] Add documentation. Signed-off-by: dblock --- DEVELOPER_GUIDE.md | 2 +- guides/auth.md | 8 ++-- opensearchpy/__init__.py | 4 +- opensearchpy/helpers/__init__.py | 4 +- opensearchpy/helpers/__init__.pyi | 2 +- opensearchpy/helpers/signer.py | 10 ++--- samples/sigv4/requests-hello.py | 4 +- samples/sigv4/urllib3-hello.py | 4 +- test_opensearchpy/test_connection.py | 56 ++++++++++++++-------------- 9 files changed, 46 insertions(+), 48 deletions(-) diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index bdcd8f08..5fe9cad1 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -88,7 +88,7 @@ You can also run individual tests matching a pattern (`pytest -k [pattern]`). ``` ./.ci/run-tests true 1.3.0 test_no_http_compression -test_opensearchpy/test_connection.py::TestUrllib3Connection::test_no_http_compression PASSED [ 33%] +test_opensearchpy/test_connection.py::TestUrllib3HttpConnection::test_no_http_compression PASSED [ 33%] test_opensearchpy/test_connection.py::TestRequestsConnection::test_no_http_compression PASSED [ 66%] test_opensearchpy/test_async/test_connection.py::TestAIOHttpConnection::test_no_http_compression PASSED [100%] ``` diff --git a/guides/auth.md b/guides/auth.md index 3e7f4092..215d47f0 100644 --- a/guides/auth.md +++ b/guides/auth.md @@ -9,24 +9,24 @@ OpenSearch allows you to use different methods for the authentication via `conne ## IAM Authentication -Opensearch-py supports IAM-based authentication via `AWSV4SignerAuth`, which uses `RequestHttpConnection` as the transport class for communicating with OpenSearch clusters running in Amazon Managed OpenSearch and OpenSearch Serverless, and works in conjunction with [botocore](https://pypi.org/project/botocore/). +Opensearch-py supports IAM-based authentication via `RequestsAWSV4SignerAuth` and `Urllib3AWSV4SignerAuth`, which use `RequestHttpConnection` and `Urllib3HttpConnection` respectively, as the transport classes for communicating with OpenSearch clusters running in Amazon Managed OpenSearch and OpenSearch Serverless, and works in conjunction with [botocore](https://pypi.org/project/botocore/). ```python -from opensearchpy import OpenSearch, RequestsHttpConnection, AWSV4SignerAuth +from opensearchpy import OpenSearch, Urllib3HttpConnection, Urllib3AWSV4SignerAuth import boto3 host = '' # cluster endpoint, for example: my-test-domain.us-east-1.es.amazonaws.com region = 'us-west-2' service = 'es' # 'aoss' for OpenSearch Serverless credentials = boto3.Session().get_credentials() -auth = AWSV4SignerAuth(credentials, region, service) +auth = Urllib3AWSV4SignerAuth(credentials, region, service) client = OpenSearch( hosts = [{'host': host, 'port': 443}], http_auth = auth, use_ssl = True, verify_certs = True, - connection_class = RequestsHttpConnection, + connection_class = Urllib3HttpConnection, pool_maxsize = 20 ) diff --git a/opensearchpy/__init__.py b/opensearchpy/__init__.py index 30751faa..6f26bc53 100644 --- a/opensearchpy/__init__.py +++ b/opensearchpy/__init__.py @@ -75,7 +75,7 @@ AWSV4SignerAsyncAuth, AWSV4SignerAuth, RequestsAWSV4SignerAuth, - UrlLib3AWSV4SignerAuth, + Urllib3AWSV4SignerAuth, ) from .helpers.aggs import A from .helpers.analysis import analyzer, char_filter, normalizer, token_filter, tokenizer @@ -171,7 +171,7 @@ "OpenSearchWarning", "OpenSearchDeprecationWarning", "AWSV4SignerAuth", - "UrlLib3AWSV4SignerAuth", + "Urllib3AWSV4SignerAuth", "RequestsAWSV4SignerAuth", "AWSV4SignerAsyncAuth", "A", diff --git a/opensearchpy/helpers/__init__.py b/opensearchpy/helpers/__init__.py index 95f92e34..80dbf8bf 100644 --- a/opensearchpy/helpers/__init__.py +++ b/opensearchpy/helpers/__init__.py @@ -39,7 +39,7 @@ ) from .asyncsigner import AWSV4SignerAsyncAuth from .errors import BulkIndexError, ScanError -from .signer import AWSV4SignerAuth, RequestsAWSV4SignerAuth, UrlLib3AWSV4SignerAuth +from .signer import AWSV4SignerAuth, RequestsAWSV4SignerAuth, Urllib3AWSV4SignerAuth __all__ = [ "BulkIndexError", @@ -55,7 +55,7 @@ "AWSV4SignerAuth", "AWSV4SignerAsyncAuth", "RequestsAWSV4SignerAuth", - "UrlLib3AWSV4SignerAuth", + "Urllib3AWSV4SignerAuth", ] diff --git a/opensearchpy/helpers/__init__.pyi b/opensearchpy/helpers/__init__.pyi index 784ac030..a4711989 100644 --- a/opensearchpy/helpers/__init__.pyi +++ b/opensearchpy/helpers/__init__.pyi @@ -48,6 +48,6 @@ try: from .._async.helpers.actions import async_streaming_bulk as async_streaming_bulk from .asyncsigner import AWSV4SignerAsyncAuth as AWSV4SignerAsyncAuth from .signer import AWSV4SignerAuth as AWSV4SignerAuth - from .signer import RequestsAWSV4SignerAuth, UrlLib3AWSV4SignerAuth + from .signer import RequestsAWSV4SignerAuth, Urllib3AWSV4SignerAuth except (ImportError, SyntaxError): pass diff --git a/opensearchpy/helpers/signer.py b/opensearchpy/helpers/signer.py index 0eeba0b8..6304f805 100644 --- a/opensearchpy/helpers/signer.py +++ b/opensearchpy/helpers/signer.py @@ -8,7 +8,7 @@ # GitHub history for details. import sys -from typing import Any, Callable +from typing import Any, Callable, Dict import requests @@ -62,9 +62,7 @@ def __init__(self, credentials, region: str, service: str = "es") -> Any: # typ raise ValueError("Service name cannot be empty") self.service = service - def sign(self, method: str, url: str, body: Any) -> dict[str, str]: - print(f"SIGN: {method}: {url} ({body})") - + def sign(self, method: str, url: str, body: Any) -> Dict[str, str]: """ This method signs the request and returns headers. :param method: HTTP method @@ -137,9 +135,9 @@ class AWSV4SignerAuth(RequestsAWSV4SignerAuth): pass -class UrlLib3AWSV4SignerAuth(Callable): # type: ignore +class Urllib3AWSV4SignerAuth(Callable): # type: ignore def __init__(self, credentials, region, service="es"): # type: ignore self.signer = AuthSigner(credentials, region, service) - def __call__(self, method: str, url: str, body: Any) -> dict[str, str]: + def __call__(self, method: str, url: str, body: Any) -> Dict[str, str]: return self.signer.sign(method, url, body) diff --git a/samples/sigv4/requests-hello.py b/samples/sigv4/requests-hello.py index c362db86..1f14f55e 100644 --- a/samples/sigv4/requests-hello.py +++ b/samples/sigv4/requests-hello.py @@ -14,7 +14,7 @@ from urllib.parse import urlparse from boto3 import Session -from opensearchpy import AWSV4SignerAuth, OpenSearch, RequestsHttpConnection +from opensearchpy import RequestsAWSV4SignerAuth, OpenSearch, RequestsHttpConnection # verbose logging logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.INFO) @@ -26,7 +26,7 @@ credentials = Session().get_credentials() -auth = AWSV4SignerAuth(credentials, region, service) +auth = RequestsAWSV4SignerAuth(credentials, region, service) client = OpenSearch( hosts=[{ diff --git a/samples/sigv4/urllib3-hello.py b/samples/sigv4/urllib3-hello.py index 09670e0d..46d6a89f 100644 --- a/samples/sigv4/urllib3-hello.py +++ b/samples/sigv4/urllib3-hello.py @@ -14,7 +14,7 @@ from urllib.parse import urlparse from boto3 import Session -from opensearchpy import UrlLib3AWSV4SignerAuth, OpenSearch, Urllib3HttpConnection +from opensearchpy import Urllib3AWSV4SignerAuth, OpenSearch, Urllib3HttpConnection # verbose logging logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.INFO) @@ -26,7 +26,7 @@ credentials = Session().get_credentials() -auth = UrlLib3AWSV4SignerAuth(credentials, region, service) +auth = Urllib3AWSV4SignerAuth(credentials, region, service) client = OpenSearch( hosts=[{ diff --git a/test_opensearchpy/test_connection.py b/test_opensearchpy/test_connection.py index d2b2a73b..7ec07a60 100644 --- a/test_opensearchpy/test_connection.py +++ b/test_opensearchpy/test_connection.py @@ -191,7 +191,7 @@ def test_no_ca_certs(self): assert Connection.default_ca_certs() is None -class TestUrllib3Connection(TestCase): +class TestUrllib3HttpConnection(TestCase): def _get_mock_connection(self, connection_params={}, response_body=b"{}"): con = Urllib3HttpConnection(**connection_params) @@ -323,14 +323,14 @@ def test_http_auth_list(self): ) @pytest.mark.skipif( - sys.version_info < (3, 6), reason="AWSV4SignerAuth requires python3.6+" + sys.version_info < (3, 6), reason="RequestsAWSV4SignerAuth requires python3.6+" ) def test_aws_signer_as_http_auth(self): region = "us-west-2" - from opensearchpy.helpers.signer import UrlLib3AWSV4SignerAuth + from opensearchpy.helpers.signer import Urllib3AWSV4SignerAuth - auth = UrlLib3AWSV4SignerAuth(self.mock_session(), region) + auth = Urllib3AWSV4SignerAuth(self.mock_session(), region) headers = auth("GET", "http://localhost", None) self.assertIn("Authorization", headers) self.assertIn("X-Amz-Date", headers) @@ -338,47 +338,47 @@ def test_aws_signer_as_http_auth(self): self.assertIn("X-Amz-Content-SHA256", headers) @pytest.mark.skipif( - sys.version_info < (3, 6), reason="AWSV4SignerAuth requires python3.6+" + sys.version_info < (3, 6), reason="RequestsAWSV4SignerAuth requires python3.6+" ) def test_aws_signer_when_region_is_null(self): session = self.mock_session() - from opensearchpy.helpers.signer import AWSV4SignerAuth + from opensearchpy.helpers.signer import RequestsAWSV4SignerAuth with pytest.raises(ValueError) as e: - AWSV4SignerAuth(session, None) + RequestsAWSV4SignerAuth(session, None) assert str(e.value) == "Region cannot be empty" with pytest.raises(ValueError) as e: - AWSV4SignerAuth(session, "") + RequestsAWSV4SignerAuth(session, "") assert str(e.value) == "Region cannot be empty" @pytest.mark.skipif( - sys.version_info < (3, 6), reason="AWSV4SignerAuth requires python3.6+" + sys.version_info < (3, 6), reason="RequestsAWSV4SignerAuth requires python3.6+" ) def test_aws_signer_when_credentials_is_null(self): region = "us-west-1" - from opensearchpy.helpers.signer import AWSV4SignerAuth + from opensearchpy.helpers.signer import RequestsAWSV4SignerAuth with pytest.raises(ValueError) as e: - AWSV4SignerAuth(None, region) + RequestsAWSV4SignerAuth(None, region) assert str(e.value) == "Credentials cannot be empty" with pytest.raises(ValueError) as e: - AWSV4SignerAuth("", region) + RequestsAWSV4SignerAuth("", region) assert str(e.value) == "Credentials cannot be empty" @pytest.mark.skipif( - sys.version_info < (3, 6), reason="AWSV4SignerAuth requires python3.6+" + sys.version_info < (3, 6), reason="Urllib3AWSV4SignerAuth requires python3.6+" ) def test_aws_signer_when_service_is_specified(self): region = "us-west-1" service = "aoss" - from opensearchpy.helpers.signer import UrlLib3AWSV4SignerAuth + from opensearchpy.helpers.signer import Urllib3AWSV4SignerAuth - auth = UrlLib3AWSV4SignerAuth(self.mock_session(), region, service) + auth = Urllib3AWSV4SignerAuth(self.mock_session(), region, service) headers = auth("GET", "http://localhost", None) self.assertIn("Authorization", headers) self.assertIn("X-Amz-Date", headers) @@ -494,7 +494,7 @@ def urlopen_raise(*_, **__): assert str(e.value) == "Wasn't modified!" -class TestSignerWithFrozenCredentials(TestUrllib3Connection): +class TestSignerWithFrozenCredentials(TestUrllib3HttpConnection): def mock_session(self): access_key = uuid.uuid4().hex secret_key = uuid.uuid4().hex @@ -508,16 +508,16 @@ def mock_session(self): return dummy_session @pytest.mark.skipif( - sys.version_info < (3, 6), reason="AWSV4SignerAuth requires python3.6+" + sys.version_info < (3, 6), reason="RequestsAWSV4SignerAuth requires python3.6+" ) def test_urllib3_http_connection_aws_signer_frozen_credentials_as_http_auth(self): region = "us-west-2" - from opensearchpy.helpers.signer import UrlLib3AWSV4SignerAuth + from opensearchpy.helpers.signer import Urllib3AWSV4SignerAuth mock_session = self.mock_session() - auth = UrlLib3AWSV4SignerAuth(mock_session, region) + auth = Urllib3AWSV4SignerAuth(mock_session, region) headers = auth("GET", "http://localhost", None) self.assertIn("Authorization", headers) self.assertIn("X-Amz-Date", headers) @@ -526,18 +526,18 @@ def test_urllib3_http_connection_aws_signer_frozen_credentials_as_http_auth(self mock_session.get_frozen_credentials.assert_called_once() @pytest.mark.skipif( - sys.version_info < (3, 6), reason="AWSV4SignerAuth requires python3.6+" + sys.version_info < (3, 6), reason="RequestsAWSV4SignerAuth requires python3.6+" ) def test_requests_http_connection_aws_signer_frozen_credentials_as_http_auth(self): region = "us-west-2" import requests - from opensearchpy.helpers.signer import AWSV4SignerAuth + from opensearchpy.helpers.signer import RequestsAWSV4SignerAuth mock_session = self.mock_session() - auth = AWSV4SignerAuth(mock_session, region) + auth = RequestsAWSV4SignerAuth(mock_session, region) con = RequestsHttpConnection(http_auth=auth) prepared_request = requests.Request("GET", "http://localhost").prepare() auth(prepared_request) @@ -919,16 +919,16 @@ def mock_session(self): return dummy_session @pytest.mark.skipif( - sys.version_info < (3, 6), reason="AWSV4SignerAuth requires python3.6+" + sys.version_info < (3, 6), reason="RequestsAWSV4SignerAuth requires python3.6+" ) def test_aws_signer_as_http_auth(self): region = "us-west-2" import requests - from opensearchpy.helpers.signer import AWSV4SignerAuth + from opensearchpy.helpers.signer import RequestsAWSV4SignerAuth - auth = AWSV4SignerAuth(self.mock_session(), region) + auth = RequestsAWSV4SignerAuth(self.mock_session(), region) con = RequestsHttpConnection(http_auth=auth) prepared_request = requests.Request("GET", "http://localhost").prepare() auth(prepared_request) @@ -939,7 +939,7 @@ def test_aws_signer_as_http_auth(self): self.assertIn("X-Amz-Content-SHA256", prepared_request.headers) @pytest.mark.skipif( - sys.version_info < (3, 6), reason="AWSV4SignerAuth requires python3.6+" + sys.version_info < (3, 6), reason="RequestsAWSV4SignerAuth requires python3.6+" ) def test_aws_signer_when_service_is_specified(self): region = "us-west-1" @@ -947,9 +947,9 @@ def test_aws_signer_when_service_is_specified(self): import requests - from opensearchpy.helpers.signer import AWSV4SignerAuth + from opensearchpy.helpers.signer import RequestsAWSV4SignerAuth - auth = AWSV4SignerAuth(self.mock_session(), region, service) + auth = RequestsAWSV4SignerAuth(self.mock_session(), region, service) con = RequestsHttpConnection(http_auth=auth) prepared_request = requests.Request("GET", "http://localhost").prepare() auth(prepared_request) From ae1ebe181fc77e879d63eea6dd2e44b022223f93 Mon Sep 17 00:00:00 2001 From: dblock Date: Fri, 20 Oct 2023 17:54:05 -0400 Subject: [PATCH 06/13] Use the correct class in tests. Signed-off-by: dblock --- test_opensearchpy/test_connection.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/test_opensearchpy/test_connection.py b/test_opensearchpy/test_connection.py index 7ec07a60..0adee61c 100644 --- a/test_opensearchpy/test_connection.py +++ b/test_opensearchpy/test_connection.py @@ -214,7 +214,7 @@ def test_ssl_context(self): # it means SSLContext is not available for that version of python # and we should skip this test. raise SkipTest( - "Test test_ssl_context is skipped cause SSLContext is not available for this version of ptyhon" + "Test test_ssl_context is skipped cause SSLContext is not available for this version of python" ) con = Urllib3HttpConnection(use_ssl=True, ssl_context=context) @@ -323,7 +323,7 @@ def test_http_auth_list(self): ) @pytest.mark.skipif( - sys.version_info < (3, 6), reason="RequestsAWSV4SignerAuth requires python3.6+" + sys.version_info < (3, 6), reason="Urllib3AWSV4SignerAuth requires python3.6+" ) def test_aws_signer_as_http_auth(self): region = "us-west-2" @@ -338,35 +338,35 @@ def test_aws_signer_as_http_auth(self): self.assertIn("X-Amz-Content-SHA256", headers) @pytest.mark.skipif( - sys.version_info < (3, 6), reason="RequestsAWSV4SignerAuth requires python3.6+" + sys.version_info < (3, 6), reason="Urllib3AWSV4SignerAuth requires python3.6+" ) def test_aws_signer_when_region_is_null(self): session = self.mock_session() - from opensearchpy.helpers.signer import RequestsAWSV4SignerAuth + from opensearchpy.helpers.signer import Urllib3AWSV4SignerAuth with pytest.raises(ValueError) as e: - RequestsAWSV4SignerAuth(session, None) + Urllib3AWSV4SignerAuth(session, None) assert str(e.value) == "Region cannot be empty" with pytest.raises(ValueError) as e: - RequestsAWSV4SignerAuth(session, "") + Urllib3AWSV4SignerAuth(session, "") assert str(e.value) == "Region cannot be empty" @pytest.mark.skipif( - sys.version_info < (3, 6), reason="RequestsAWSV4SignerAuth requires python3.6+" + sys.version_info < (3, 6), reason="Urllib3AWSV4SignerAuth requires python3.6+" ) def test_aws_signer_when_credentials_is_null(self): region = "us-west-1" - from opensearchpy.helpers.signer import RequestsAWSV4SignerAuth + from opensearchpy.helpers.signer import Urllib3AWSV4SignerAuth with pytest.raises(ValueError) as e: - RequestsAWSV4SignerAuth(None, region) + Urllib3AWSV4SignerAuth(None, region) assert str(e.value) == "Credentials cannot be empty" with pytest.raises(ValueError) as e: - RequestsAWSV4SignerAuth("", region) + Urllib3AWSV4SignerAuth("", region) assert str(e.value) == "Credentials cannot be empty" @pytest.mark.skipif( @@ -508,7 +508,7 @@ def mock_session(self): return dummy_session @pytest.mark.skipif( - sys.version_info < (3, 6), reason="RequestsAWSV4SignerAuth requires python3.6+" + sys.version_info < (3, 6), reason="Urllib3AWSV4SignerAuth requires python3.6+" ) def test_urllib3_http_connection_aws_signer_frozen_credentials_as_http_auth(self): region = "us-west-2" From df5b29b5230bd404f48dc1f1873e5e732cedd240 Mon Sep 17 00:00:00 2001 From: dblock Date: Mon, 23 Oct 2023 11:45:16 -0400 Subject: [PATCH 07/13] Renamed samples. Signed-off-by: dblock --- samples/{sigv4 => aws}/README.md | 2 +- samples/{sigv4/requests-hello.py => aws/search-requests.py} | 0 samples/{sigv4/urllib3-hello.py => aws/search-urllib3.py} | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename samples/{sigv4 => aws}/README.md (93%) rename samples/{sigv4/requests-hello.py => aws/search-requests.py} (100%) rename samples/{sigv4/urllib3-hello.py => aws/search-urllib3.py} (100%) diff --git a/samples/sigv4/README.md b/samples/aws/README.md similarity index 93% rename from samples/sigv4/README.md rename to samples/aws/README.md index cb5d0eae..17ad4ee0 100644 --- a/samples/sigv4/README.md +++ b/samples/aws/README.md @@ -11,7 +11,7 @@ export AWS_REGION=us-west-2 export SERVICE=es # use "aoss" for OpenSearch Serverless. export ENDPOINT=https://....us-west-2.es.amazonaws.com -poetry run sigv4/requests-hello.py +poetry run aws/search-urllib.py ``` This will output the version of OpenSearch and a search result. diff --git a/samples/sigv4/requests-hello.py b/samples/aws/search-requests.py similarity index 100% rename from samples/sigv4/requests-hello.py rename to samples/aws/search-requests.py diff --git a/samples/sigv4/urllib3-hello.py b/samples/aws/search-urllib3.py similarity index 100% rename from samples/sigv4/urllib3-hello.py rename to samples/aws/search-urllib3.py From 8d5aeb09c87b786095f420d978e4e53c460f6a72 Mon Sep 17 00:00:00 2001 From: dblock Date: Mon, 23 Oct 2023 12:05:01 -0400 Subject: [PATCH 08/13] Split up requests and urllib3 unit tests. Signed-off-by: dblock --- .../test_async/test_connection.py | 8 +- test_opensearchpy/test_connection.py | 1231 ----------------- test_opensearchpy/test_connection/__init__.py | 25 + .../test_connection/test_base_connection.py | 231 ++++ .../test_requests_http_connection.py | 538 +++++++ .../test_urllib3_http_connection.py | 383 +++++ 6 files changed, 1179 insertions(+), 1237 deletions(-) delete mode 100644 test_opensearchpy/test_connection.py create mode 100644 test_opensearchpy/test_connection/__init__.py create mode 100644 test_opensearchpy/test_connection/test_base_connection.py create mode 100644 test_opensearchpy/test_connection/test_requests_http_connection.py create mode 100644 test_opensearchpy/test_connection/test_urllib3_http_connection.py diff --git a/test_opensearchpy/test_async/test_connection.py b/test_opensearchpy/test_async/test_connection.py index 147a6a3a..3df51645 100644 --- a/test_opensearchpy/test_async/test_connection.py +++ b/test_opensearchpy/test_async/test_connection.py @@ -48,11 +48,6 @@ pytestmark = pytest.mark.asyncio -def gzip_decompress(data): - buf = gzip.GzipFile(fileobj=io.BytesIO(data), mode="rb") - return buf.read() - - class TestAIOHttpConnection: async def _get_mock_connection( self, @@ -130,7 +125,8 @@ async def test_http_compression(self): _, kwargs = con.session.request.call_args - assert gzip_decompress(kwargs["data"]) == b"{}" + buf = gzip.GzipFile(fileobj=io.BytesIO(kwargs["data"]), mode="rb") + assert buf.read() == b"{}" assert kwargs["headers"]["accept-encoding"] == "gzip,deflate" assert kwargs["headers"]["content-encoding"] == "gzip" diff --git a/test_opensearchpy/test_connection.py b/test_opensearchpy/test_connection.py deleted file mode 100644 index 0adee61c..00000000 --- a/test_opensearchpy/test_connection.py +++ /dev/null @@ -1,1231 +0,0 @@ -# -*- coding: utf-8 -*- -# SPDX-License-Identifier: Apache-2.0 -# -# The OpenSearch Contributors require contributions made to -# this file be licensed under the Apache-2.0 license or a -# compatible open source license. -# -# Modifications Copyright OpenSearch Contributors. See -# GitHub history for details. -# -# Licensed to Elasticsearch B.V. under one or more contributor -# license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright -# ownership. Elasticsearch B.V. licenses this file to you under -# the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - - -import gzip -import io -import json -import os -import re -import ssl -import sys -import unittest -import uuid -import warnings -from platform import python_version - -import pytest -import six -import urllib3 -from mock import Mock, patch -from requests.auth import AuthBase -from urllib3._collections import HTTPHeaderDict - -from opensearchpy import __versionstr__ -from opensearchpy.compat import reraise_exceptions -from opensearchpy.connection import ( - Connection, - RequestsHttpConnection, - Urllib3HttpConnection, -) -from opensearchpy.exceptions import ( - ConflictError, - ConnectionError, - NotFoundError, - RequestError, - TransportError, -) - -from .test_cases import SkipTest, TestCase - -try: - from pytest import MonkeyPatch -except ImportError: # Old version of pytest for 2.7 and 3.5 - from _pytest.monkeypatch import MonkeyPatch - -from pytest import raises - -from opensearchpy import OpenSearch, serializer -from opensearchpy.connection import connections - -if sys.version_info > (3, 0): - from test_opensearchpy.TestHttpServer import TestHTTPServer - - -def gzip_decompress(data): - buf = gzip.GzipFile(fileobj=io.BytesIO(data), mode="rb") - return buf.read() - - -class TestBaseConnection(TestCase): - def test_empty_warnings(self): - con = Connection() - with warnings.catch_warnings(record=True) as w: - con._raise_warnings(()) - con._raise_warnings([]) - - self.assertEqual(w, []) - - def test_raises_warnings(self): - con = Connection() - - with warnings.catch_warnings(record=True) as warn: - con._raise_warnings(['299 OpenSearch-7.6.1-aa751 "this is deprecated"']) - - self.assertEqual([str(w.message) for w in warn], ["this is deprecated"]) - - with warnings.catch_warnings(record=True) as warn: - con._raise_warnings( - [ - '299 OpenSearch-7.6.1-aa751 "this is also deprecated"', - '299 OpenSearch-7.6.1-aa751 "this is also deprecated"', - '299 OpenSearch-7.6.1-aa751 "guess what? deprecated"', - ] - ) - - self.assertEqual( - [str(w.message) for w in warn], - ["this is also deprecated", "guess what? deprecated"], - ) - - def test_raises_warnings_when_folded(self): - con = Connection() - with warnings.catch_warnings(record=True) as warn: - con._raise_warnings( - [ - '299 OpenSearch-7.6.1-aa751 "warning",' - '299 OpenSearch-7.6.1-aa751 "folded"', - ] - ) - - self.assertEqual([str(w.message) for w in warn], ["warning", "folded"]) - - @unittest.skipIf(six.PY2, "not compatible with python2") - def test_raises_errors(self): - con = Connection() - with self.assertLogs("opensearch") as captured, self.assertRaises( - NotFoundError - ): - con._raise_error(404, "Not found", "application/json") - self.assertEqual(len(captured.output), 1) - - # NB: this should assertNoLogs() but that method is not available until python3.10 - with self.assertRaises(NotFoundError): - con._raise_error(404, "Not found", "text/plain; charset=UTF-8") - - def test_ipv6_host_and_port(self): - for kwargs, expected_host in [ - ({"host": "::1"}, "http://[::1]:9200"), - ({"host": "::1", "port": 443}, "http://[::1]:443"), - ({"host": "::1", "use_ssl": True}, "https://[::1]:9200"), - ({"host": "127.0.0.1", "port": 1234}, "http://127.0.0.1:1234"), - ({"host": "localhost", "use_ssl": True}, "https://localhost:9200"), - ]: - conn = Connection(**kwargs) - assert conn.host == expected_host - - def test_compatibility_accept_header(self): - try: - conn = Connection() - assert "accept" not in conn.headers - - os.environ["ELASTIC_CLIENT_APIVERSIONING"] = "0" - - conn = Connection() - assert "accept" not in conn.headers - - os.environ["ELASTIC_CLIENT_APIVERSIONING"] = "1" - - conn = Connection() - assert ( - conn.headers["accept"] - == "application/vnd.elasticsearch+json;compatible-with=7" - ) - finally: - os.environ.pop("ELASTIC_CLIENT_APIVERSIONING") - - def test_ca_certs_ssl_cert_file(self): - cert = "/path/to/clientcert.pem" - with MonkeyPatch().context() as monkeypatch: - monkeypatch.setenv("SSL_CERT_FILE", cert) - assert Connection.default_ca_certs() == cert - - def test_ca_certs_ssl_cert_dir(self): - cert = "/path/to/clientcert/dir" - with MonkeyPatch().context() as monkeypatch: - monkeypatch.setenv("SSL_CERT_DIR", cert) - assert Connection.default_ca_certs() == cert - - def test_ca_certs_certifi(self): - import certifi - - assert Connection.default_ca_certs() == certifi.where() - - def test_no_ca_certs(self): - with MonkeyPatch().context() as monkeypatch: - monkeypatch.setitem(sys.modules, "certifi", None) - assert Connection.default_ca_certs() is None - - -class TestUrllib3HttpConnection(TestCase): - def _get_mock_connection(self, connection_params={}, response_body=b"{}"): - con = Urllib3HttpConnection(**connection_params) - - def _dummy_urlopen(*args, **kwargs): - dummy_response = Mock() - dummy_response.headers = HTTPHeaderDict({}) - dummy_response.status = 200 - dummy_response.data = response_body - _dummy_urlopen.call_args = (args, kwargs) - return dummy_response - - con.pool.urlopen = _dummy_urlopen - return con - - def test_ssl_context(self): - try: - context = ssl.create_default_context() - except AttributeError: - # if create_default_context raises an AttributeError Exception - # it means SSLContext is not available for that version of python - # and we should skip this test. - raise SkipTest( - "Test test_ssl_context is skipped cause SSLContext is not available for this version of python" - ) - - con = Urllib3HttpConnection(use_ssl=True, ssl_context=context) - self.assertEqual(len(con.pool.conn_kw.keys()), 1) - self.assertIsInstance(con.pool.conn_kw["ssl_context"], ssl.SSLContext) - self.assertTrue(con.use_ssl) - - def test_opaque_id(self): - con = Urllib3HttpConnection(opaque_id="app-1") - self.assertEqual(con.headers["x-opaque-id"], "app-1") - - def test_no_http_compression(self): - con = self._get_mock_connection() - self.assertFalse(con.http_compress) - self.assertNotIn("accept-encoding", con.headers) - - con.perform_request("GET", "/") - - (_, _, req_body), kwargs = con.pool.urlopen.call_args - - self.assertFalse(req_body) - self.assertNotIn("accept-encoding", kwargs["headers"]) - self.assertNotIn("content-encoding", kwargs["headers"]) - - def test_http_compression(self): - con = self._get_mock_connection({"http_compress": True}) - self.assertTrue(con.http_compress) - self.assertEqual(con.headers["accept-encoding"], "gzip,deflate") - - # 'content-encoding' shouldn't be set at a connection level. - # Should be applied only if the request is sent with a body. - self.assertNotIn("content-encoding", con.headers) - - con.perform_request("GET", "/", body=b"{}") - - (_, _, req_body), kwargs = con.pool.urlopen.call_args - - self.assertEqual(gzip_decompress(req_body), b"{}") - self.assertEqual(kwargs["headers"]["accept-encoding"], "gzip,deflate") - self.assertEqual(kwargs["headers"]["content-encoding"], "gzip") - - con.perform_request("GET", "/") - - (_, _, req_body), kwargs = con.pool.urlopen.call_args - - self.assertFalse(req_body) - self.assertEqual(kwargs["headers"]["accept-encoding"], "gzip,deflate") - self.assertNotIn("content-encoding", kwargs["headers"]) - - def test_default_user_agent(self): - con = Urllib3HttpConnection() - self.assertEqual( - con._get_default_user_agent(), - "opensearch-py/%s (Python %s)" % (__versionstr__, python_version()), - ) - - def test_timeout_set(self): - con = Urllib3HttpConnection(timeout=42) - self.assertEqual(42, con.timeout) - - def test_keep_alive_is_on_by_default(self): - con = Urllib3HttpConnection() - self.assertEqual( - { - "connection": "keep-alive", - "content-type": "application/json", - "user-agent": con._get_default_user_agent(), - }, - con.headers, - ) - - def test_http_auth(self): - con = Urllib3HttpConnection(http_auth="username:secret") - self.assertEqual( - { - "authorization": "Basic dXNlcm5hbWU6c2VjcmV0", - "connection": "keep-alive", - "content-type": "application/json", - "user-agent": con._get_default_user_agent(), - }, - con.headers, - ) - - def test_http_auth_tuple(self): - con = Urllib3HttpConnection(http_auth=("username", "secret")) - self.assertEqual( - { - "authorization": "Basic dXNlcm5hbWU6c2VjcmV0", - "content-type": "application/json", - "connection": "keep-alive", - "user-agent": con._get_default_user_agent(), - }, - con.headers, - ) - - def test_http_auth_list(self): - con = Urllib3HttpConnection(http_auth=["username", "secret"]) - self.assertEqual( - { - "authorization": "Basic dXNlcm5hbWU6c2VjcmV0", - "content-type": "application/json", - "connection": "keep-alive", - "user-agent": con._get_default_user_agent(), - }, - con.headers, - ) - - @pytest.mark.skipif( - sys.version_info < (3, 6), reason="Urllib3AWSV4SignerAuth requires python3.6+" - ) - def test_aws_signer_as_http_auth(self): - region = "us-west-2" - - from opensearchpy.helpers.signer import Urllib3AWSV4SignerAuth - - auth = Urllib3AWSV4SignerAuth(self.mock_session(), region) - headers = auth("GET", "http://localhost", None) - self.assertIn("Authorization", headers) - self.assertIn("X-Amz-Date", headers) - self.assertIn("X-Amz-Security-Token", headers) - self.assertIn("X-Amz-Content-SHA256", headers) - - @pytest.mark.skipif( - sys.version_info < (3, 6), reason="Urllib3AWSV4SignerAuth requires python3.6+" - ) - def test_aws_signer_when_region_is_null(self): - session = self.mock_session() - - from opensearchpy.helpers.signer import Urllib3AWSV4SignerAuth - - with pytest.raises(ValueError) as e: - Urllib3AWSV4SignerAuth(session, None) - assert str(e.value) == "Region cannot be empty" - - with pytest.raises(ValueError) as e: - Urllib3AWSV4SignerAuth(session, "") - assert str(e.value) == "Region cannot be empty" - - @pytest.mark.skipif( - sys.version_info < (3, 6), reason="Urllib3AWSV4SignerAuth requires python3.6+" - ) - def test_aws_signer_when_credentials_is_null(self): - region = "us-west-1" - - from opensearchpy.helpers.signer import Urllib3AWSV4SignerAuth - - with pytest.raises(ValueError) as e: - Urllib3AWSV4SignerAuth(None, region) - assert str(e.value) == "Credentials cannot be empty" - - with pytest.raises(ValueError) as e: - Urllib3AWSV4SignerAuth("", region) - assert str(e.value) == "Credentials cannot be empty" - - @pytest.mark.skipif( - sys.version_info < (3, 6), reason="Urllib3AWSV4SignerAuth requires python3.6+" - ) - def test_aws_signer_when_service_is_specified(self): - region = "us-west-1" - service = "aoss" - - from opensearchpy.helpers.signer import Urllib3AWSV4SignerAuth - - auth = Urllib3AWSV4SignerAuth(self.mock_session(), region, service) - headers = auth("GET", "http://localhost", None) - self.assertIn("Authorization", headers) - self.assertIn("X-Amz-Date", headers) - self.assertIn("X-Amz-Security-Token", headers) - - def mock_session(self): - access_key = uuid.uuid4().hex - secret_key = uuid.uuid4().hex - token = uuid.uuid4().hex - dummy_session = Mock() - dummy_session.access_key = access_key - dummy_session.secret_key = secret_key - dummy_session.token = token - del dummy_session.get_frozen_credentials - - return dummy_session - - def test_uses_https_if_verify_certs_is_off(self): - with warnings.catch_warnings(record=True) as w: - con = Urllib3HttpConnection(use_ssl=True, verify_certs=False) - self.assertEqual(1, len(w)) - self.assertEqual( - "Connecting to https://localhost:9200 using SSL with verify_certs=False is insecure.", - str(w[0].message), - ) - - self.assertIsInstance(con.pool, urllib3.HTTPSConnectionPool) - - def test_nowarn_when_uses_https_if_verify_certs_is_off(self): - with warnings.catch_warnings(record=True) as w: - con = Urllib3HttpConnection( - use_ssl=True, verify_certs=False, ssl_show_warn=False - ) - self.assertEqual(0, len(w)) - - self.assertIsInstance(con.pool, urllib3.HTTPSConnectionPool) - - def test_doesnt_use_https_if_not_specified(self): - con = Urllib3HttpConnection() - self.assertIsInstance(con.pool, urllib3.HTTPConnectionPool) - - def test_no_warning_when_using_ssl_context(self): - ctx = ssl.create_default_context() - with warnings.catch_warnings(record=True) as w: - Urllib3HttpConnection(ssl_context=ctx) - self.assertEqual(0, len(w)) - - def test_warns_if_using_non_default_ssl_kwargs_with_ssl_context(self): - for kwargs in ( - {"ssl_show_warn": False}, - {"ssl_show_warn": True}, - {"verify_certs": True}, - {"verify_certs": False}, - {"ca_certs": "/path/to/certs"}, - {"ssl_show_warn": True, "ca_certs": "/path/to/certs"}, - ): - kwargs["ssl_context"] = ssl.create_default_context() - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - - Urllib3HttpConnection(**kwargs) - - self.assertEqual(1, len(w)) - self.assertEqual( - "When using `ssl_context`, all other SSL related kwargs are ignored", - str(w[0].message), - ) - - def test_uses_given_ca_certs(self): - path = "/path/to/my/ca_certs.pem" - c = Urllib3HttpConnection(use_ssl=True, ca_certs=path) - self.assertEqual(path, c.pool.ca_certs) - - def test_uses_default_ca_certs(self): - c = Urllib3HttpConnection(use_ssl=True) - self.assertEqual(Connection.default_ca_certs(), c.pool.ca_certs) - - def test_uses_no_ca_certs(self): - c = Urllib3HttpConnection(use_ssl=True, verify_certs=False) - self.assertIsNone(c.pool.ca_certs) - - @patch("opensearchpy.connection.base.logger") - def test_uncompressed_body_logged(self, logger): - con = self._get_mock_connection(connection_params={"http_compress": True}) - con.perform_request("GET", "/", body=b'{"example": "body"}') - - self.assertEqual(2, logger.debug.call_count) - req, resp = logger.debug.call_args_list - - self.assertEqual('> {"example": "body"}', req[0][0] % req[0][1:]) - self.assertEqual("< {}", resp[0][0] % resp[0][1:]) - - def test_surrogatepass_into_bytes(self): - buf = b"\xe4\xbd\xa0\xe5\xa5\xbd\xed\xa9\xaa" - con = self._get_mock_connection(response_body=buf) - status, headers, data = con.perform_request("GET", "/") - self.assertEqual(u"你好\uda6a", data) # fmt: skip - - @pytest.mark.skipif( - not reraise_exceptions, reason="RecursionError isn't defined in Python <3.5" - ) - def test_recursion_error_reraised(self): - conn = Urllib3HttpConnection() - - def urlopen_raise(*_, **__): - raise RecursionError("Wasn't modified!") - - conn.pool.urlopen = urlopen_raise - - with pytest.raises(RecursionError) as e: - conn.perform_request("GET", "/") - assert str(e.value) == "Wasn't modified!" - - -class TestSignerWithFrozenCredentials(TestUrllib3HttpConnection): - def mock_session(self): - access_key = uuid.uuid4().hex - secret_key = uuid.uuid4().hex - token = uuid.uuid4().hex - dummy_session = Mock() - dummy_session.access_key = access_key - dummy_session.secret_key = secret_key - dummy_session.token = token - dummy_session.get_frozen_credentials = Mock(return_value=dummy_session) - - return dummy_session - - @pytest.mark.skipif( - sys.version_info < (3, 6), reason="Urllib3AWSV4SignerAuth requires python3.6+" - ) - def test_urllib3_http_connection_aws_signer_frozen_credentials_as_http_auth(self): - region = "us-west-2" - - from opensearchpy.helpers.signer import Urllib3AWSV4SignerAuth - - mock_session = self.mock_session() - - auth = Urllib3AWSV4SignerAuth(mock_session, region) - headers = auth("GET", "http://localhost", None) - self.assertIn("Authorization", headers) - self.assertIn("X-Amz-Date", headers) - self.assertIn("X-Amz-Security-Token", headers) - self.assertIn("X-Amz-Content-SHA256", headers) - mock_session.get_frozen_credentials.assert_called_once() - - @pytest.mark.skipif( - sys.version_info < (3, 6), reason="RequestsAWSV4SignerAuth requires python3.6+" - ) - def test_requests_http_connection_aws_signer_frozen_credentials_as_http_auth(self): - region = "us-west-2" - - import requests - - from opensearchpy.helpers.signer import RequestsAWSV4SignerAuth - - mock_session = self.mock_session() - - auth = RequestsAWSV4SignerAuth(mock_session, region) - con = RequestsHttpConnection(http_auth=auth) - prepared_request = requests.Request("GET", "http://localhost").prepare() - auth(prepared_request) - self.assertEqual(auth, con.session.auth) - self.assertIn("Authorization", prepared_request.headers) - self.assertIn("X-Amz-Date", prepared_request.headers) - self.assertIn("X-Amz-Security-Token", prepared_request.headers) - self.assertIn("X-Amz-Content-SHA256", prepared_request.headers) - mock_session.get_frozen_credentials.assert_called_once() - - -class TestRequestsHttpConnection(TestCase): - def _get_mock_connection( - self, connection_params={}, status_code=200, response_body=b"{}" - ): - con = RequestsHttpConnection(**connection_params) - - def _dummy_send(*args, **kwargs): - dummy_response = Mock() - dummy_response.headers = {} - dummy_response.status_code = status_code - dummy_response.content = response_body - dummy_response.request = args[0] - dummy_response.cookies = {} - _dummy_send.call_args = (args, kwargs) - return dummy_response - - con.session.send = _dummy_send - return con - - def _get_request(self, connection, *args, **kwargs): - if "body" in kwargs: - kwargs["body"] = kwargs["body"].encode("utf-8") - - status, headers, data = connection.perform_request(*args, **kwargs) - self.assertEqual(200, status) - self.assertEqual("{}", data) - - timeout = kwargs.pop("timeout", connection.timeout) - args, kwargs = connection.session.send.call_args - self.assertEqual(timeout, kwargs["timeout"]) - self.assertEqual(1, len(args)) - return args[0] - - def test_custom_http_auth_is_allowed(self): - auth = AuthBase() - c = RequestsHttpConnection(http_auth=auth) - - self.assertEqual(auth, c.session.auth) - - def test_timeout_set(self): - con = RequestsHttpConnection(timeout=42) - self.assertEqual(42, con.timeout) - - def test_opaque_id(self): - con = RequestsHttpConnection(opaque_id="app-1") - self.assertEqual(con.headers["x-opaque-id"], "app-1") - - def test_no_http_compression(self): - con = self._get_mock_connection() - - self.assertFalse(con.http_compress) - self.assertNotIn("content-encoding", con.session.headers) - - con.perform_request("GET", "/") - - req = con.session.send.call_args[0][0] - self.assertNotIn("content-encoding", req.headers) - self.assertNotIn("accept-encoding", req.headers) - - def test_http_compression(self): - con = self._get_mock_connection( - {"http_compress": True}, - ) - - self.assertTrue(con.http_compress) - - # 'content-encoding' shouldn't be set at a session level. - # Should be applied only if the request is sent with a body. - self.assertNotIn("content-encoding", con.session.headers) - - con.perform_request("GET", "/", body=b"{}") - - req = con.session.send.call_args[0][0] - self.assertEqual(req.headers["content-encoding"], "gzip") - self.assertEqual(req.headers["accept-encoding"], "gzip,deflate") - - con.perform_request("GET", "/") - - req = con.session.send.call_args[0][0] - self.assertNotIn("content-encoding", req.headers) - self.assertEqual(req.headers["accept-encoding"], "gzip,deflate") - - def test_uses_https_if_verify_certs_is_off(self): - with warnings.catch_warnings(record=True) as w: - con = self._get_mock_connection( - {"use_ssl": True, "url_prefix": "url", "verify_certs": False} - ) - self.assertEqual(1, len(w)) - self.assertEqual( - "Connecting to https://localhost:9200 using SSL with verify_certs=False is insecure.", - str(w[0].message), - ) - - request = self._get_request(con, "GET", "/") - - self.assertEqual("https://localhost:9200/url/", request.url) - self.assertEqual("GET", request.method) - self.assertEqual(None, request.body) - - def test_uses_given_ca_certs(self): - path = "/path/to/my/ca_certs.pem" - c = RequestsHttpConnection(ca_certs=path) - self.assertEqual(path, c.session.verify) - - def test_uses_default_ca_certs(self): - c = RequestsHttpConnection() - self.assertEqual(Connection.default_ca_certs(), c.session.verify) - - def test_uses_no_ca_certs(self): - c = RequestsHttpConnection(verify_certs=False) - self.assertFalse(c.session.verify) - - def test_nowarn_when_uses_https_if_verify_certs_is_off(self): - with warnings.catch_warnings(record=True) as w: - con = self._get_mock_connection( - { - "use_ssl": True, - "url_prefix": "url", - "verify_certs": False, - "ssl_show_warn": False, - } - ) - self.assertEqual(0, len(w)) - - request = self._get_request(con, "GET", "/") - - self.assertEqual("https://localhost:9200/url/", request.url) - self.assertEqual("GET", request.method) - self.assertEqual(None, request.body) - - def test_merge_headers(self): - con = self._get_mock_connection( - connection_params={"headers": {"h1": "v1", "h2": "v2"}} - ) - req = self._get_request(con, "GET", "/", headers={"h2": "v2p", "h3": "v3"}) - self.assertEqual(req.headers["h1"], "v1") - self.assertEqual(req.headers["h2"], "v2p") - self.assertEqual(req.headers["h3"], "v3") - - def test_default_headers(self): - con = self._get_mock_connection() - req = self._get_request(con, "GET", "/") - self.assertEqual(req.headers["content-type"], "application/json") - self.assertEqual(req.headers["user-agent"], con._get_default_user_agent()) - - def test_custom_headers(self): - con = self._get_mock_connection() - req = self._get_request( - con, - "GET", - "/", - headers={ - "content-type": "application/x-ndjson", - "user-agent": "custom-agent/1.2.3", - }, - ) - self.assertEqual(req.headers["content-type"], "application/x-ndjson") - self.assertEqual(req.headers["user-agent"], "custom-agent/1.2.3") - - def test_http_auth(self): - con = RequestsHttpConnection(http_auth="username:secret") - self.assertEqual(("username", "secret"), con.session.auth) - - def test_http_auth_tuple(self): - con = RequestsHttpConnection(http_auth=("username", "secret")) - self.assertEqual(("username", "secret"), con.session.auth) - - def test_http_auth_list(self): - con = RequestsHttpConnection(http_auth=["username", "secret"]) - self.assertEqual(("username", "secret"), con.session.auth) - - def test_repr(self): - con = self._get_mock_connection({"host": "opensearchpy.com", "port": 443}) - self.assertEqual( - "", repr(con) - ) - - def test_conflict_error_is_returned_on_409(self): - con = self._get_mock_connection(status_code=409) - self.assertRaises(ConflictError, con.perform_request, "GET", "/", {}, "") - - def test_not_found_error_is_returned_on_404(self): - con = self._get_mock_connection(status_code=404) - self.assertRaises(NotFoundError, con.perform_request, "GET", "/", {}, "") - - def test_request_error_is_returned_on_400(self): - con = self._get_mock_connection(status_code=400) - self.assertRaises(RequestError, con.perform_request, "GET", "/", {}, "") - - @patch("opensearchpy.connection.base.logger") - def test_head_with_404_doesnt_get_logged(self, logger): - con = self._get_mock_connection(status_code=404) - self.assertRaises(NotFoundError, con.perform_request, "HEAD", "/", {}, "") - self.assertEqual(0, logger.warning.call_count) - - @patch("opensearchpy.connection.base.tracer") - @patch("opensearchpy.connection.base.logger") - def test_failed_request_logs_and_traces(self, logger, tracer): - con = self._get_mock_connection( - response_body=b'{"answer": 42}', status_code=500 - ) - self.assertRaises( - TransportError, - con.perform_request, - "GET", - "/", - {"param": 42}, - "{}".encode("utf-8"), - ) - - # trace request - self.assertEqual(1, tracer.info.call_count) - # trace response - self.assertEqual(1, tracer.debug.call_count) - # log url and duration - self.assertEqual(1, logger.warning.call_count) - self.assertTrue( - re.match( - r"^GET http://localhost:9200/\?param=42 \[status:500 request:0.[0-9]{3}s\]", - logger.warning.call_args[0][0] % logger.warning.call_args[0][1:], - ) - ) - - @patch("opensearchpy.connection.base.tracer") - @patch("opensearchpy.connection.base.logger") - def test_success_logs_and_traces(self, logger, tracer): - con = self._get_mock_connection(response_body=b"""{"answer": "that's it!"}""") - status, headers, data = con.perform_request( - "GET", - "/", - {"param": 42}, - """{"question": "what's that?"}""".encode("utf-8"), - ) - - # trace request - self.assertEqual(1, tracer.info.call_count) - self.assertEqual( - """curl -H 'Content-Type: application/json' -XGET 'http://localhost:9200/?pretty¶m=42' -d '{\n "question": "what\\u0027s that?"\n}'""", - tracer.info.call_args[0][0] % tracer.info.call_args[0][1:], - ) - # trace response - self.assertEqual(1, tracer.debug.call_count) - self.assertTrue( - re.match( - r'#\[200\] \(0.[0-9]{3}s\)\n#{\n# "answer": "that\\u0027s it!"\n#}', - tracer.debug.call_args[0][0] % tracer.debug.call_args[0][1:], - ) - ) - - # log url and duration - self.assertEqual(1, logger.info.call_count) - self.assertTrue( - re.match( - r"GET http://localhost:9200/\?param=42 \[status:200 request:0.[0-9]{3}s\]", - logger.info.call_args[0][0] % logger.info.call_args[0][1:], - ) - ) - # log request body and response - self.assertEqual(2, logger.debug.call_count) - req, resp = logger.debug.call_args_list - self.assertEqual('> {"question": "what\'s that?"}', req[0][0] % req[0][1:]) - self.assertEqual('< {"answer": "that\'s it!"}', resp[0][0] % resp[0][1:]) - - @patch("opensearchpy.connection.base.logger") - def test_uncompressed_body_logged(self, logger): - con = self._get_mock_connection(connection_params={"http_compress": True}) - con.perform_request("GET", "/", body=b'{"example": "body"}') - - self.assertEqual(2, logger.debug.call_count) - req, resp = logger.debug.call_args_list - self.assertEqual('> {"example": "body"}', req[0][0] % req[0][1:]) - self.assertEqual("< {}", resp[0][0] % resp[0][1:]) - - con = self._get_mock_connection( - connection_params={"http_compress": True}, - status_code=500, - response_body=b'{"hello":"world"}', - ) - with pytest.raises(TransportError): - con.perform_request("GET", "/", body=b'{"example": "body2"}') - - self.assertEqual(4, logger.debug.call_count) - _, _, req, resp = logger.debug.call_args_list - self.assertEqual('> {"example": "body2"}', req[0][0] % req[0][1:]) - self.assertEqual('< {"hello":"world"}', resp[0][0] % resp[0][1:]) - - def test_defaults(self): - con = self._get_mock_connection() - request = self._get_request(con, "GET", "/") - - self.assertEqual("http://localhost:9200/", request.url) - self.assertEqual("GET", request.method) - self.assertEqual(None, request.body) - - def test_params_properly_encoded(self): - con = self._get_mock_connection() - request = self._get_request( - con, "GET", "/", params={"param": "value with spaces"} - ) - - self.assertEqual("http://localhost:9200/?param=value+with+spaces", request.url) - self.assertEqual("GET", request.method) - self.assertEqual(None, request.body) - - def test_body_attached(self): - con = self._get_mock_connection() - request = self._get_request(con, "GET", "/", body='{"answer": 42}') - - self.assertEqual("http://localhost:9200/", request.url) - self.assertEqual("GET", request.method) - self.assertEqual('{"answer": 42}'.encode("utf-8"), request.body) - - def test_http_auth_attached(self): - con = self._get_mock_connection({"http_auth": "username:secret"}) - request = self._get_request(con, "GET", "/") - - self.assertEqual(request.headers["authorization"], "Basic dXNlcm5hbWU6c2VjcmV0") - - @patch("opensearchpy.connection.base.tracer") - def test_url_prefix(self, tracer): - con = self._get_mock_connection({"url_prefix": "/some-prefix/"}) - request = self._get_request( - con, "GET", "/_search", body='{"answer": 42}', timeout=0.1 - ) - - self.assertEqual("http://localhost:9200/some-prefix/_search", request.url) - self.assertEqual("GET", request.method) - self.assertEqual('{"answer": 42}'.encode("utf-8"), request.body) - - # trace request - self.assertEqual(1, tracer.info.call_count) - self.assertEqual( - "curl -H 'Content-Type: application/json' -XGET 'http://localhost:9200/_search?pretty' -d '{\n \"answer\": 42\n}'", - tracer.info.call_args[0][0] % tracer.info.call_args[0][1:], - ) - - def test_surrogatepass_into_bytes(self): - buf = b"\xe4\xbd\xa0\xe5\xa5\xbd\xed\xa9\xaa" - con = self._get_mock_connection(response_body=buf) - status, headers, data = con.perform_request("GET", "/") - self.assertEqual(u"你好\uda6a", data) # fmt: skip - - @pytest.mark.skipif( - not reraise_exceptions, reason="RecursionError isn't defined in Python <3.5" - ) - def test_recursion_error_reraised(self): - conn = RequestsHttpConnection() - - def send_raise(*_, **__): - raise RecursionError("Wasn't modified!") - - conn.session.send = send_raise - - with pytest.raises(RecursionError) as e: - conn.perform_request("GET", "/") - assert str(e.value) == "Wasn't modified!" - - def mock_session(self): - access_key = uuid.uuid4().hex - secret_key = uuid.uuid4().hex - token = uuid.uuid4().hex - dummy_session = Mock() - dummy_session.access_key = access_key - dummy_session.secret_key = secret_key - dummy_session.token = token - del dummy_session.get_frozen_credentials - - return dummy_session - - @pytest.mark.skipif( - sys.version_info < (3, 6), reason="RequestsAWSV4SignerAuth requires python3.6+" - ) - def test_aws_signer_as_http_auth(self): - region = "us-west-2" - - import requests - - from opensearchpy.helpers.signer import RequestsAWSV4SignerAuth - - auth = RequestsAWSV4SignerAuth(self.mock_session(), region) - con = RequestsHttpConnection(http_auth=auth) - prepared_request = requests.Request("GET", "http://localhost").prepare() - auth(prepared_request) - self.assertEqual(auth, con.session.auth) - self.assertIn("Authorization", prepared_request.headers) - self.assertIn("X-Amz-Date", prepared_request.headers) - self.assertIn("X-Amz-Security-Token", prepared_request.headers) - self.assertIn("X-Amz-Content-SHA256", prepared_request.headers) - - @pytest.mark.skipif( - sys.version_info < (3, 6), reason="RequestsAWSV4SignerAuth requires python3.6+" - ) - def test_aws_signer_when_service_is_specified(self): - region = "us-west-1" - service = "aoss" - - import requests - - from opensearchpy.helpers.signer import RequestsAWSV4SignerAuth - - auth = RequestsAWSV4SignerAuth(self.mock_session(), region, service) - con = RequestsHttpConnection(http_auth=auth) - prepared_request = requests.Request("GET", "http://localhost").prepare() - auth(prepared_request) - self.assertEqual(auth, con.session.auth) - self.assertIn("Authorization", prepared_request.headers) - self.assertIn("X-Amz-Date", prepared_request.headers) - self.assertIn("X-Amz-Security-Token", prepared_request.headers) - - -@pytest.mark.skipif( - sys.version_info < (3, 0), - reason="http_server is only available from python 3.x", -) -class TestConnectionHttpServer: - """Tests the HTTP connection implementations against a live server E2E""" - - @classmethod - def setup_class(cls): - # Start server - cls.server = TestHTTPServer(port=8080) - cls.server.start() - - @classmethod - def teardown_class(cls): - # Stop server - cls.server.stop() - - def httpserver(self, conn, **kwargs): - status, headers, data = conn.perform_request("GET", "/", **kwargs) - data = json.loads(data) - return (status, data) - - def test_urllib3_connection(self): - # Defaults - conn = Urllib3HttpConnection("localhost", port=8080, use_ssl=False, timeout=60) - user_agent = conn._get_default_user_agent() - status, data = self.httpserver(conn) - assert status == 200 - assert data["method"] == "GET" - assert data["headers"] == { - "Accept-Encoding": "identity", - "Content-Type": "application/json", - "Host": "localhost:8080", - "User-Agent": user_agent, - } - - # http_compress=False - conn = Urllib3HttpConnection( - "localhost", port=8080, use_ssl=False, http_compress=False, timeout=60 - ) - status, data = self.httpserver(conn) - assert status == 200 - assert data["method"] == "GET" - assert data["headers"] == { - "Accept-Encoding": "identity", - "Content-Type": "application/json", - "Host": "localhost:8080", - "User-Agent": user_agent, - } - - # http_compress=True - conn = Urllib3HttpConnection( - "localhost", port=8080, use_ssl=False, http_compress=True, timeout=60 - ) - status, data = self.httpserver(conn) - assert status == 200 - assert data["headers"] == { - "Accept-Encoding": "gzip,deflate", - "Content-Type": "application/json", - "Host": "localhost:8080", - "User-Agent": user_agent, - } - - # Headers - conn = Urllib3HttpConnection( - "localhost", - port=8080, - use_ssl=False, - http_compress=True, - headers={"header1": "value1"}, - timeout=60, - ) - status, data = self.httpserver( - conn, headers={"header2": "value2", "header1": "override!"} - ) - assert status == 200 - assert data["headers"] == { - "Accept-Encoding": "gzip,deflate", - "Content-Type": "application/json", - "Host": "localhost:8080", - "Header1": "override!", - "Header2": "value2", - "User-Agent": user_agent, - } - - def test_urllib3_connection_error(self): - conn = Urllib3HttpConnection("not.a.host.name") - with pytest.raises(ConnectionError): - conn.perform_request("GET", "/") - - def test_requests_connection(self): - # Defaults - conn = RequestsHttpConnection("localhost", port=8080, use_ssl=False, timeout=60) - user_agent = conn._get_default_user_agent() - status, data = self.httpserver(conn) - assert status == 200 - assert data["method"] == "GET" - assert data["headers"] == { - "Accept-Encoding": "identity", - "Content-Type": "application/json", - "Host": "localhost:8080", - "User-Agent": user_agent, - } - - # http_compress=False - conn = RequestsHttpConnection( - "localhost", port=8080, use_ssl=False, http_compress=False, timeout=60 - ) - status, data = self.httpserver(conn) - assert status == 200 - assert data["method"] == "GET" - assert data["headers"] == { - "Accept-Encoding": "identity", - "Content-Type": "application/json", - "Host": "localhost:8080", - "User-Agent": user_agent, - } - - # http_compress=True - conn = RequestsHttpConnection( - "localhost", port=8080, use_ssl=False, http_compress=True, timeout=60 - ) - status, data = self.httpserver(conn) - assert status == 200 - assert data["headers"] == { - "Accept-Encoding": "gzip,deflate", - "Content-Type": "application/json", - "Host": "localhost:8080", - "User-Agent": user_agent, - } - - # Headers - conn = RequestsHttpConnection( - "localhost", - port=8080, - use_ssl=False, - http_compress=True, - headers={"header1": "value1"}, - timeout=60, - ) - status, data = self.httpserver( - conn, headers={"header2": "value2", "header1": "override!"} - ) - assert status == 200 - assert data["headers"] == { - "Accept-Encoding": "gzip,deflate", - "Content-Type": "application/json", - "Host": "localhost:8080", - "Header1": "override!", - "Header2": "value2", - "User-Agent": user_agent, - } - - def test_requests_connection_error(self): - conn = RequestsHttpConnection("not.a.host.name") - with pytest.raises(ConnectionError): - conn.perform_request("GET", "/") - - -@pytest.mark.skipif( - sys.version_info < (3, 0), - reason="http_server is only available from python 3.x", -) -class TestRequestsConnectionRedirect: - @classmethod - def setup_class(cls): - # Start servers - cls.server1 = TestHTTPServer(port=8080) - cls.server1.start() - cls.server2 = TestHTTPServer(port=8090) - cls.server2.start() - - @classmethod - def teardown_class(cls): - # Stop servers - cls.server2.stop() - cls.server1.stop() - - # allow_redirects = False - def test_redirect_failure_when_allow_redirect_false(self): - conn = RequestsHttpConnection("localhost", port=8080, use_ssl=False, timeout=60) - with pytest.raises(TransportError) as e: - conn.perform_request("GET", "/redirect", allow_redirects=False) - assert e.value.status_code == 302 - - # allow_redirects = True (Default) - def test_redirect_success_when_allow_redirect_true(self): - conn = RequestsHttpConnection("localhost", port=8080, use_ssl=False, timeout=60) - user_agent = conn._get_default_user_agent() - status, headers, data = conn.perform_request("GET", "/redirect") - assert status == 200 - data = json.loads(data) - assert data["headers"] == { - "Host": "localhost:8090", - "Accept-Encoding": "identity", - "User-Agent": user_agent, - } - - -def test_default_connection_is_returned_by_default(): - c = connections.Connections() - - con, con2 = object(), object() - c.add_connection("default", con) - - c.add_connection("not-default", con2) - - assert c.get_connection() is con - - -def test_get_connection_created_connection_if_needed(): - c = connections.Connections() - c.configure(default={"hosts": ["opensearch.com"]}, local={"hosts": ["localhost"]}) - - default = c.get_connection() - local = c.get_connection("local") - - assert isinstance(default, OpenSearch) - assert isinstance(local, OpenSearch) - - assert [{"host": "opensearch.com"}] == default.transport.hosts - assert [{"host": "localhost"}] == local.transport.hosts - - -def test_configure_preserves_unchanged_connections(): - c = connections.Connections() - - c.configure(default={"hosts": ["opensearch.com"]}, local={"hosts": ["localhost"]}) - default = c.get_connection() - local = c.get_connection("local") - - c.configure( - default={"hosts": ["not-opensearch.com"]}, local={"hosts": ["localhost"]} - ) - new_default = c.get_connection() - new_local = c.get_connection("local") - - assert new_local is local - assert new_default is not default - - -def test_remove_connection_removes_both_conn_and_conf(): - c = connections.Connections() - - c.configure(default={"hosts": ["opensearch.com"]}, local={"hosts": ["localhost"]}) - c.add_connection("local2", object()) - - c.remove_connection("default") - c.get_connection("local2") - c.remove_connection("local2") - - with raises(Exception): - c.get_connection("local2") - c.get_connection("default") - - -def test_create_connection_constructs_client(): - c = connections.Connections() - c.create_connection("testing", hosts=["opensearch.com"]) - - con = c.get_connection("testing") - assert [{"host": "opensearch.com"}] == con.transport.hosts - - -def test_create_connection_adds_our_serializer(): - c = connections.Connections() - c.create_connection("testing", hosts=["opensearch.com"]) - - assert c.get_connection("testing").transport.serializer is serializer.serializer diff --git a/test_opensearchpy/test_connection/__init__.py b/test_opensearchpy/test_connection/__init__.py new file mode 100644 index 00000000..7e52ae22 --- /dev/null +++ b/test_opensearchpy/test_connection/__init__.py @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# The OpenSearch Contributors require contributions made to +# this file be licensed under the Apache-2.0 license or a +# compatible open source license. +# +# Modifications Copyright OpenSearch Contributors. See +# GitHub history for details. +# +# Licensed to Elasticsearch B.V. under one or more contributor +# license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright +# ownership. Elasticsearch B.V. licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/test_opensearchpy/test_connection/test_base_connection.py b/test_opensearchpy/test_connection/test_base_connection.py new file mode 100644 index 00000000..63729206 --- /dev/null +++ b/test_opensearchpy/test_connection/test_base_connection.py @@ -0,0 +1,231 @@ +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: Apache-2.0 +# +# The OpenSearch Contributors require contributions made to +# this file be licensed under the Apache-2.0 license or a +# compatible open source license. +# +# Modifications Copyright OpenSearch Contributors. See +# GitHub history for details. +# +# Licensed to Elasticsearch B.V. under one or more contributor +# license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright +# ownership. Elasticsearch B.V. licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + + +import os +import sys +import unittest +import warnings + +import six + +from opensearchpy.connection import Connection +from opensearchpy.exceptions import NotFoundError + +from ..test_cases import TestCase + +try: + from pytest import MonkeyPatch +except ImportError: # Old version of pytest for 2.7 and 3.5 + from _pytest.monkeypatch import MonkeyPatch + +from pytest import raises + +from opensearchpy import OpenSearch, serializer +from opensearchpy.connection import connections + + +class TestBaseConnection(TestCase): + def test_empty_warnings(self): + con = Connection() + with warnings.catch_warnings(record=True) as w: + con._raise_warnings(()) + con._raise_warnings([]) + + self.assertEqual(w, []) + + def test_raises_warnings(self): + con = Connection() + + with warnings.catch_warnings(record=True) as warn: + con._raise_warnings(['299 OpenSearch-7.6.1-aa751 "this is deprecated"']) + + self.assertEqual([str(w.message) for w in warn], ["this is deprecated"]) + + with warnings.catch_warnings(record=True) as warn: + con._raise_warnings( + [ + '299 OpenSearch-7.6.1-aa751 "this is also deprecated"', + '299 OpenSearch-7.6.1-aa751 "this is also deprecated"', + '299 OpenSearch-7.6.1-aa751 "guess what? deprecated"', + ] + ) + + self.assertEqual( + [str(w.message) for w in warn], + ["this is also deprecated", "guess what? deprecated"], + ) + + def test_raises_warnings_when_folded(self): + con = Connection() + with warnings.catch_warnings(record=True) as warn: + con._raise_warnings( + [ + '299 OpenSearch-7.6.1-aa751 "warning",' + '299 OpenSearch-7.6.1-aa751 "folded"', + ] + ) + + self.assertEqual([str(w.message) for w in warn], ["warning", "folded"]) + + @unittest.skipIf(six.PY2, "not compatible with python2") + def test_raises_errors(self): + con = Connection() + with self.assertLogs("opensearch") as captured, self.assertRaises( + NotFoundError + ): + con._raise_error(404, "Not found", "application/json") + self.assertEqual(len(captured.output), 1) + + # NB: this should assertNoLogs() but that method is not available until python3.10 + with self.assertRaises(NotFoundError): + con._raise_error(404, "Not found", "text/plain; charset=UTF-8") + + def test_ipv6_host_and_port(self): + for kwargs, expected_host in [ + ({"host": "::1"}, "http://[::1]:9200"), + ({"host": "::1", "port": 443}, "http://[::1]:443"), + ({"host": "::1", "use_ssl": True}, "https://[::1]:9200"), + ({"host": "127.0.0.1", "port": 1234}, "http://127.0.0.1:1234"), + ({"host": "localhost", "use_ssl": True}, "https://localhost:9200"), + ]: + conn = Connection(**kwargs) + assert conn.host == expected_host + + def test_compatibility_accept_header(self): + try: + conn = Connection() + assert "accept" not in conn.headers + + os.environ["ELASTIC_CLIENT_APIVERSIONING"] = "0" + + conn = Connection() + assert "accept" not in conn.headers + + os.environ["ELASTIC_CLIENT_APIVERSIONING"] = "1" + + conn = Connection() + assert ( + conn.headers["accept"] + == "application/vnd.elasticsearch+json;compatible-with=7" + ) + finally: + os.environ.pop("ELASTIC_CLIENT_APIVERSIONING") + + def test_ca_certs_ssl_cert_file(self): + cert = "/path/to/clientcert.pem" + with MonkeyPatch().context() as monkeypatch: + monkeypatch.setenv("SSL_CERT_FILE", cert) + assert Connection.default_ca_certs() == cert + + def test_ca_certs_ssl_cert_dir(self): + cert = "/path/to/clientcert/dir" + with MonkeyPatch().context() as monkeypatch: + monkeypatch.setenv("SSL_CERT_DIR", cert) + assert Connection.default_ca_certs() == cert + + def test_ca_certs_certifi(self): + import certifi + + assert Connection.default_ca_certs() == certifi.where() + + def test_no_ca_certs(self): + with MonkeyPatch().context() as monkeypatch: + monkeypatch.setitem(sys.modules, "certifi", None) + assert Connection.default_ca_certs() is None + + def test_default_connection_is_returned_by_default(self): + c = connections.Connections() + + con, con2 = object(), object() + c.add_connection("default", con) + + c.add_connection("not-default", con2) + + assert c.get_connection() is con + + def test_get_connection_created_connection_if_needed(self): + c = connections.Connections() + c.configure( + default={"hosts": ["opensearch.com"]}, local={"hosts": ["localhost"]} + ) + + default = c.get_connection() + local = c.get_connection("local") + + assert isinstance(default, OpenSearch) + assert isinstance(local, OpenSearch) + + assert [{"host": "opensearch.com"}] == default.transport.hosts + assert [{"host": "localhost"}] == local.transport.hosts + + def test_configure_preserves_unchanged_connections(self): + c = connections.Connections() + + c.configure( + default={"hosts": ["opensearch.com"]}, local={"hosts": ["localhost"]} + ) + default = c.get_connection() + local = c.get_connection("local") + + c.configure( + default={"hosts": ["not-opensearch.com"]}, local={"hosts": ["localhost"]} + ) + new_default = c.get_connection() + new_local = c.get_connection("local") + + assert new_local is local + assert new_default is not default + + def test_remove_connection_removes_both_conn_and_conf(self): + c = connections.Connections() + + c.configure( + default={"hosts": ["opensearch.com"]}, local={"hosts": ["localhost"]} + ) + c.add_connection("local2", object()) + + c.remove_connection("default") + c.get_connection("local2") + c.remove_connection("local2") + + with raises(Exception): + c.get_connection("local2") + c.get_connection("default") + + def test_create_connection_constructs_client(self): + c = connections.Connections() + c.create_connection("testing", hosts=["opensearch.com"]) + + con = c.get_connection("testing") + assert [{"host": "opensearch.com"}] == con.transport.hosts + + def test_create_connection_adds_our_serializer(self): + c = connections.Connections() + c.create_connection("testing", hosts=["opensearch.com"]) + + assert c.get_connection("testing").transport.serializer is serializer.serializer diff --git a/test_opensearchpy/test_connection/test_requests_http_connection.py b/test_opensearchpy/test_connection/test_requests_http_connection.py new file mode 100644 index 00000000..0b552ee0 --- /dev/null +++ b/test_opensearchpy/test_connection/test_requests_http_connection.py @@ -0,0 +1,538 @@ +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: Apache-2.0 +# +# The OpenSearch Contributors require contributions made to +# this file be licensed under the Apache-2.0 license or a +# compatible open source license. +# +# Modifications Copyright OpenSearch Contributors. See +# GitHub history for details. +# +# Licensed to Elasticsearch B.V. under one or more contributor +# license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright +# ownership. Elasticsearch B.V. licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + + +import json +import re +import sys +import uuid +import warnings + +import pytest +from mock import Mock, patch +from requests.auth import AuthBase + +from opensearchpy.compat import reraise_exceptions +from opensearchpy.connection import Connection, RequestsHttpConnection +from opensearchpy.exceptions import ( + ConflictError, + NotFoundError, + RequestError, + TransportError, +) + +from ..test_cases import TestCase + +if sys.version_info > (3, 0): + from test_opensearchpy.TestHttpServer import TestHTTPServer + + +class TestRequestsHttpConnection(TestCase): + def _get_mock_connection( + self, connection_params={}, status_code=200, response_body=b"{}" + ): + con = RequestsHttpConnection(**connection_params) + + def _dummy_send(*args, **kwargs): + dummy_response = Mock() + dummy_response.headers = {} + dummy_response.status_code = status_code + dummy_response.content = response_body + dummy_response.request = args[0] + dummy_response.cookies = {} + _dummy_send.call_args = (args, kwargs) + return dummy_response + + con.session.send = _dummy_send + return con + + def _get_request(self, connection, *args, **kwargs): + if "body" in kwargs: + kwargs["body"] = kwargs["body"].encode("utf-8") + + status, headers, data = connection.perform_request(*args, **kwargs) + self.assertEqual(200, status) + self.assertEqual("{}", data) + + timeout = kwargs.pop("timeout", connection.timeout) + args, kwargs = connection.session.send.call_args + self.assertEqual(timeout, kwargs["timeout"]) + self.assertEqual(1, len(args)) + return args[0] + + def test_custom_http_auth_is_allowed(self): + auth = AuthBase() + c = RequestsHttpConnection(http_auth=auth) + + self.assertEqual(auth, c.session.auth) + + def test_timeout_set(self): + con = RequestsHttpConnection(timeout=42) + self.assertEqual(42, con.timeout) + + def test_opaque_id(self): + con = RequestsHttpConnection(opaque_id="app-1") + self.assertEqual(con.headers["x-opaque-id"], "app-1") + + def test_no_http_compression(self): + con = self._get_mock_connection() + + self.assertFalse(con.http_compress) + self.assertNotIn("content-encoding", con.session.headers) + + con.perform_request("GET", "/") + + req = con.session.send.call_args[0][0] + self.assertNotIn("content-encoding", req.headers) + self.assertNotIn("accept-encoding", req.headers) + + def test_http_compression(self): + con = self._get_mock_connection( + {"http_compress": True}, + ) + + self.assertTrue(con.http_compress) + + # 'content-encoding' shouldn't be set at a session level. + # Should be applied only if the request is sent with a body. + self.assertNotIn("content-encoding", con.session.headers) + + con.perform_request("GET", "/", body=b"{}") + + req = con.session.send.call_args[0][0] + self.assertEqual(req.headers["content-encoding"], "gzip") + self.assertEqual(req.headers["accept-encoding"], "gzip,deflate") + + con.perform_request("GET", "/") + + req = con.session.send.call_args[0][0] + self.assertNotIn("content-encoding", req.headers) + self.assertEqual(req.headers["accept-encoding"], "gzip,deflate") + + def test_uses_https_if_verify_certs_is_off(self): + with warnings.catch_warnings(record=True) as w: + con = self._get_mock_connection( + {"use_ssl": True, "url_prefix": "url", "verify_certs": False} + ) + self.assertEqual(1, len(w)) + self.assertEqual( + "Connecting to https://localhost:9200 using SSL with verify_certs=False is insecure.", + str(w[0].message), + ) + + request = self._get_request(con, "GET", "/") + + self.assertEqual("https://localhost:9200/url/", request.url) + self.assertEqual("GET", request.method) + self.assertEqual(None, request.body) + + def test_uses_given_ca_certs(self): + path = "/path/to/my/ca_certs.pem" + c = RequestsHttpConnection(ca_certs=path) + self.assertEqual(path, c.session.verify) + + def test_uses_default_ca_certs(self): + c = RequestsHttpConnection() + self.assertEqual(Connection.default_ca_certs(), c.session.verify) + + def test_uses_no_ca_certs(self): + c = RequestsHttpConnection(verify_certs=False) + self.assertFalse(c.session.verify) + + def test_nowarn_when_uses_https_if_verify_certs_is_off(self): + with warnings.catch_warnings(record=True) as w: + con = self._get_mock_connection( + { + "use_ssl": True, + "url_prefix": "url", + "verify_certs": False, + "ssl_show_warn": False, + } + ) + self.assertEqual(0, len(w)) + + request = self._get_request(con, "GET", "/") + + self.assertEqual("https://localhost:9200/url/", request.url) + self.assertEqual("GET", request.method) + self.assertEqual(None, request.body) + + def test_merge_headers(self): + con = self._get_mock_connection( + connection_params={"headers": {"h1": "v1", "h2": "v2"}} + ) + req = self._get_request(con, "GET", "/", headers={"h2": "v2p", "h3": "v3"}) + self.assertEqual(req.headers["h1"], "v1") + self.assertEqual(req.headers["h2"], "v2p") + self.assertEqual(req.headers["h3"], "v3") + + def test_default_headers(self): + con = self._get_mock_connection() + req = self._get_request(con, "GET", "/") + self.assertEqual(req.headers["content-type"], "application/json") + self.assertEqual(req.headers["user-agent"], con._get_default_user_agent()) + + def test_custom_headers(self): + con = self._get_mock_connection() + req = self._get_request( + con, + "GET", + "/", + headers={ + "content-type": "application/x-ndjson", + "user-agent": "custom-agent/1.2.3", + }, + ) + self.assertEqual(req.headers["content-type"], "application/x-ndjson") + self.assertEqual(req.headers["user-agent"], "custom-agent/1.2.3") + + def test_http_auth(self): + con = RequestsHttpConnection(http_auth="username:secret") + self.assertEqual(("username", "secret"), con.session.auth) + + def test_http_auth_tuple(self): + con = RequestsHttpConnection(http_auth=("username", "secret")) + self.assertEqual(("username", "secret"), con.session.auth) + + def test_http_auth_list(self): + con = RequestsHttpConnection(http_auth=["username", "secret"]) + self.assertEqual(("username", "secret"), con.session.auth) + + def test_repr(self): + con = self._get_mock_connection({"host": "opensearchpy.com", "port": 443}) + self.assertEqual( + "", repr(con) + ) + + def test_conflict_error_is_returned_on_409(self): + con = self._get_mock_connection(status_code=409) + self.assertRaises(ConflictError, con.perform_request, "GET", "/", {}, "") + + def test_not_found_error_is_returned_on_404(self): + con = self._get_mock_connection(status_code=404) + self.assertRaises(NotFoundError, con.perform_request, "GET", "/", {}, "") + + def test_request_error_is_returned_on_400(self): + con = self._get_mock_connection(status_code=400) + self.assertRaises(RequestError, con.perform_request, "GET", "/", {}, "") + + @patch("opensearchpy.connection.base.logger") + def test_head_with_404_doesnt_get_logged(self, logger): + con = self._get_mock_connection(status_code=404) + self.assertRaises(NotFoundError, con.perform_request, "HEAD", "/", {}, "") + self.assertEqual(0, logger.warning.call_count) + + @patch("opensearchpy.connection.base.tracer") + @patch("opensearchpy.connection.base.logger") + def test_failed_request_logs_and_traces(self, logger, tracer): + con = self._get_mock_connection( + response_body=b'{"answer": 42}', status_code=500 + ) + self.assertRaises( + TransportError, + con.perform_request, + "GET", + "/", + {"param": 42}, + "{}".encode("utf-8"), + ) + + # trace request + self.assertEqual(1, tracer.info.call_count) + # trace response + self.assertEqual(1, tracer.debug.call_count) + # log url and duration + self.assertEqual(1, logger.warning.call_count) + self.assertTrue( + re.match( + r"^GET http://localhost:9200/\?param=42 \[status:500 request:0.[0-9]{3}s\]", + logger.warning.call_args[0][0] % logger.warning.call_args[0][1:], + ) + ) + + @patch("opensearchpy.connection.base.tracer") + @patch("opensearchpy.connection.base.logger") + def test_success_logs_and_traces(self, logger, tracer): + con = self._get_mock_connection(response_body=b"""{"answer": "that's it!"}""") + status, headers, data = con.perform_request( + "GET", + "/", + {"param": 42}, + """{"question": "what's that?"}""".encode("utf-8"), + ) + + # trace request + self.assertEqual(1, tracer.info.call_count) + self.assertEqual( + """curl -H 'Content-Type: application/json' -XGET 'http://localhost:9200/?pretty¶m=42' -d '{\n "question": "what\\u0027s that?"\n}'""", + tracer.info.call_args[0][0] % tracer.info.call_args[0][1:], + ) + # trace response + self.assertEqual(1, tracer.debug.call_count) + self.assertTrue( + re.match( + r'#\[200\] \(0.[0-9]{3}s\)\n#{\n# "answer": "that\\u0027s it!"\n#}', + tracer.debug.call_args[0][0] % tracer.debug.call_args[0][1:], + ) + ) + + # log url and duration + self.assertEqual(1, logger.info.call_count) + self.assertTrue( + re.match( + r"GET http://localhost:9200/\?param=42 \[status:200 request:0.[0-9]{3}s\]", + logger.info.call_args[0][0] % logger.info.call_args[0][1:], + ) + ) + # log request body and response + self.assertEqual(2, logger.debug.call_count) + req, resp = logger.debug.call_args_list + self.assertEqual('> {"question": "what\'s that?"}', req[0][0] % req[0][1:]) + self.assertEqual('< {"answer": "that\'s it!"}', resp[0][0] % resp[0][1:]) + + @patch("opensearchpy.connection.base.logger") + def test_uncompressed_body_logged(self, logger): + con = self._get_mock_connection(connection_params={"http_compress": True}) + con.perform_request("GET", "/", body=b'{"example": "body"}') + + self.assertEqual(2, logger.debug.call_count) + req, resp = logger.debug.call_args_list + self.assertEqual('> {"example": "body"}', req[0][0] % req[0][1:]) + self.assertEqual("< {}", resp[0][0] % resp[0][1:]) + + con = self._get_mock_connection( + connection_params={"http_compress": True}, + status_code=500, + response_body=b'{"hello":"world"}', + ) + with pytest.raises(TransportError): + con.perform_request("GET", "/", body=b'{"example": "body2"}') + + self.assertEqual(4, logger.debug.call_count) + _, _, req, resp = logger.debug.call_args_list + self.assertEqual('> {"example": "body2"}', req[0][0] % req[0][1:]) + self.assertEqual('< {"hello":"world"}', resp[0][0] % resp[0][1:]) + + def test_defaults(self): + con = self._get_mock_connection() + request = self._get_request(con, "GET", "/") + + self.assertEqual("http://localhost:9200/", request.url) + self.assertEqual("GET", request.method) + self.assertEqual(None, request.body) + + def test_params_properly_encoded(self): + con = self._get_mock_connection() + request = self._get_request( + con, "GET", "/", params={"param": "value with spaces"} + ) + + self.assertEqual("http://localhost:9200/?param=value+with+spaces", request.url) + self.assertEqual("GET", request.method) + self.assertEqual(None, request.body) + + def test_body_attached(self): + con = self._get_mock_connection() + request = self._get_request(con, "GET", "/", body='{"answer": 42}') + + self.assertEqual("http://localhost:9200/", request.url) + self.assertEqual("GET", request.method) + self.assertEqual('{"answer": 42}'.encode("utf-8"), request.body) + + def test_http_auth_attached(self): + con = self._get_mock_connection({"http_auth": "username:secret"}) + request = self._get_request(con, "GET", "/") + + self.assertEqual(request.headers["authorization"], "Basic dXNlcm5hbWU6c2VjcmV0") + + @patch("opensearchpy.connection.base.tracer") + def test_url_prefix(self, tracer): + con = self._get_mock_connection({"url_prefix": "/some-prefix/"}) + request = self._get_request( + con, "GET", "/_search", body='{"answer": 42}', timeout=0.1 + ) + + self.assertEqual("http://localhost:9200/some-prefix/_search", request.url) + self.assertEqual("GET", request.method) + self.assertEqual('{"answer": 42}'.encode("utf-8"), request.body) + + # trace request + self.assertEqual(1, tracer.info.call_count) + self.assertEqual( + "curl -H 'Content-Type: application/json' -XGET 'http://localhost:9200/_search?pretty' -d '{\n \"answer\": 42\n}'", + tracer.info.call_args[0][0] % tracer.info.call_args[0][1:], + ) + + def test_surrogatepass_into_bytes(self): + buf = b"\xe4\xbd\xa0\xe5\xa5\xbd\xed\xa9\xaa" + con = self._get_mock_connection(response_body=buf) + status, headers, data = con.perform_request("GET", "/") + self.assertEqual(u"你好\uda6a", data) # fmt: skip + + @pytest.mark.skipif( + not reraise_exceptions, reason="RecursionError isn't defined in Python <3.5" + ) + def test_recursion_error_reraised(self): + conn = RequestsHttpConnection() + + def send_raise(*_, **__): + raise RecursionError("Wasn't modified!") + + conn.session.send = send_raise + + with pytest.raises(RecursionError) as e: + conn.perform_request("GET", "/") + assert str(e.value) == "Wasn't modified!" + + def mock_session(self): + access_key = uuid.uuid4().hex + secret_key = uuid.uuid4().hex + token = uuid.uuid4().hex + dummy_session = Mock() + dummy_session.access_key = access_key + dummy_session.secret_key = secret_key + dummy_session.token = token + del dummy_session.get_frozen_credentials + + return dummy_session + + @pytest.mark.skipif( + sys.version_info < (3, 6), reason="RequestsAWSV4SignerAuth requires python3.6+" + ) + def test_aws_signer_as_http_auth(self): + region = "us-west-2" + + import requests + + from opensearchpy.helpers.signer import RequestsAWSV4SignerAuth + + auth = RequestsAWSV4SignerAuth(self.mock_session(), region) + con = RequestsHttpConnection(http_auth=auth) + prepared_request = requests.Request("GET", "http://localhost").prepare() + auth(prepared_request) + self.assertEqual(auth, con.session.auth) + self.assertIn("Authorization", prepared_request.headers) + self.assertIn("X-Amz-Date", prepared_request.headers) + self.assertIn("X-Amz-Security-Token", prepared_request.headers) + self.assertIn("X-Amz-Content-SHA256", prepared_request.headers) + + @pytest.mark.skipif( + sys.version_info < (3, 6), reason="RequestsAWSV4SignerAuth requires python3.6+" + ) + def test_aws_signer_when_service_is_specified(self): + region = "us-west-1" + service = "aoss" + + import requests + + from opensearchpy.helpers.signer import RequestsAWSV4SignerAuth + + auth = RequestsAWSV4SignerAuth(self.mock_session(), region, service) + con = RequestsHttpConnection(http_auth=auth) + prepared_request = requests.Request("GET", "http://localhost").prepare() + auth(prepared_request) + self.assertEqual(auth, con.session.auth) + self.assertIn("Authorization", prepared_request.headers) + self.assertIn("X-Amz-Date", prepared_request.headers) + self.assertIn("X-Amz-Security-Token", prepared_request.headers) + + +@pytest.mark.skipif( + sys.version_info < (3, 0), + reason="http_server is only available from python 3.x", +) +class TestRequestsConnectionRedirect: + @classmethod + def setup_class(cls): + # Start servers + cls.server1 = TestHTTPServer(port=8080) + cls.server1.start() + cls.server2 = TestHTTPServer(port=8090) + cls.server2.start() + + @classmethod + def teardown_class(cls): + # Stop servers + cls.server2.stop() + cls.server1.stop() + + # allow_redirects = False + def test_redirect_failure_when_allow_redirect_false(self): + conn = RequestsHttpConnection("localhost", port=8080, use_ssl=False, timeout=60) + with pytest.raises(TransportError) as e: + conn.perform_request("GET", "/redirect", allow_redirects=False) + assert e.value.status_code == 302 + + # allow_redirects = True (Default) + def test_redirect_success_when_allow_redirect_true(self): + conn = RequestsHttpConnection("localhost", port=8080, use_ssl=False, timeout=60) + user_agent = conn._get_default_user_agent() + status, headers, data = conn.perform_request("GET", "/redirect") + assert status == 200 + data = json.loads(data) + assert data["headers"] == { + "Host": "localhost:8090", + "Accept-Encoding": "identity", + "User-Agent": user_agent, + } + + +class TestSignerWithFrozenCredentials(TestRequestsHttpConnection): + def mock_session(self): + access_key = uuid.uuid4().hex + secret_key = uuid.uuid4().hex + token = uuid.uuid4().hex + dummy_session = Mock() + dummy_session.access_key = access_key + dummy_session.secret_key = secret_key + dummy_session.token = token + dummy_session.get_frozen_credentials = Mock(return_value=dummy_session) + + return dummy_session + + @pytest.mark.skipif( + sys.version_info < (3, 6), reason="RequestsAWSV4SignerAuth requires python3.6+" + ) + def test_requests_http_connection_aws_signer_frozen_credentials_as_http_auth(self): + region = "us-west-2" + + import requests + + from opensearchpy.helpers.signer import RequestsAWSV4SignerAuth + + mock_session = self.mock_session() + + auth = RequestsAWSV4SignerAuth(mock_session, region) + con = RequestsHttpConnection(http_auth=auth) + prepared_request = requests.Request("GET", "http://localhost").prepare() + auth(prepared_request) + self.assertEqual(auth, con.session.auth) + self.assertIn("Authorization", prepared_request.headers) + self.assertIn("X-Amz-Date", prepared_request.headers) + self.assertIn("X-Amz-Security-Token", prepared_request.headers) + self.assertIn("X-Amz-Content-SHA256", prepared_request.headers) + mock_session.get_frozen_credentials.assert_called_once() diff --git a/test_opensearchpy/test_connection/test_urllib3_http_connection.py b/test_opensearchpy/test_connection/test_urllib3_http_connection.py new file mode 100644 index 00000000..b3ae0bca --- /dev/null +++ b/test_opensearchpy/test_connection/test_urllib3_http_connection.py @@ -0,0 +1,383 @@ +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: Apache-2.0 +# +# The OpenSearch Contributors require contributions made to +# this file be licensed under the Apache-2.0 license or a +# compatible open source license. +# +# Modifications Copyright OpenSearch Contributors. See +# GitHub history for details. +# +# Licensed to Elasticsearch B.V. under one or more contributor +# license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright +# ownership. Elasticsearch B.V. licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + + +import ssl +import sys +import uuid +import warnings +from gzip import GzipFile +from io import BytesIO +from platform import python_version + +import pytest +import urllib3 +from mock import Mock, patch +from urllib3._collections import HTTPHeaderDict + +from opensearchpy import __versionstr__ +from opensearchpy.compat import reraise_exceptions +from opensearchpy.connection import Connection, Urllib3HttpConnection + +from ..test_cases import SkipTest, TestCase + + +class TestUrllib3HttpConnection(TestCase): + def _get_mock_connection(self, connection_params={}, response_body=b"{}"): + con = Urllib3HttpConnection(**connection_params) + + def _dummy_urlopen(*args, **kwargs): + dummy_response = Mock() + dummy_response.headers = HTTPHeaderDict({}) + dummy_response.status = 200 + dummy_response.data = response_body + _dummy_urlopen.call_args = (args, kwargs) + return dummy_response + + con.pool.urlopen = _dummy_urlopen + return con + + def test_ssl_context(self): + try: + context = ssl.create_default_context() + except AttributeError: + # if create_default_context raises an AttributeError Exception + # it means SSLContext is not available for that version of python + # and we should skip this test. + raise SkipTest( + "Test test_ssl_context is skipped cause SSLContext is not available for this version of python" + ) + + con = Urllib3HttpConnection(use_ssl=True, ssl_context=context) + self.assertEqual(len(con.pool.conn_kw.keys()), 1) + self.assertIsInstance(con.pool.conn_kw["ssl_context"], ssl.SSLContext) + self.assertTrue(con.use_ssl) + + def test_opaque_id(self): + con = Urllib3HttpConnection(opaque_id="app-1") + self.assertEqual(con.headers["x-opaque-id"], "app-1") + + def test_no_http_compression(self): + con = self._get_mock_connection() + self.assertFalse(con.http_compress) + self.assertNotIn("accept-encoding", con.headers) + + con.perform_request("GET", "/") + + (_, _, req_body), kwargs = con.pool.urlopen.call_args + + self.assertFalse(req_body) + self.assertNotIn("accept-encoding", kwargs["headers"]) + self.assertNotIn("content-encoding", kwargs["headers"]) + + def test_http_compression(self): + con = self._get_mock_connection({"http_compress": True}) + self.assertTrue(con.http_compress) + self.assertEqual(con.headers["accept-encoding"], "gzip,deflate") + + # 'content-encoding' shouldn't be set at a connection level. + # Should be applied only if the request is sent with a body. + self.assertNotIn("content-encoding", con.headers) + + con.perform_request("GET", "/", body=b"{}") + + (_, _, req_body), kwargs = con.pool.urlopen.call_args + + buf = GzipFile(fileobj=BytesIO(req_body), mode="rb") + + self.assertEqual(buf.read(), b"{}") + self.assertEqual(kwargs["headers"]["accept-encoding"], "gzip,deflate") + self.assertEqual(kwargs["headers"]["content-encoding"], "gzip") + + con.perform_request("GET", "/") + + (_, _, req_body), kwargs = con.pool.urlopen.call_args + + self.assertFalse(req_body) + self.assertEqual(kwargs["headers"]["accept-encoding"], "gzip,deflate") + self.assertNotIn("content-encoding", kwargs["headers"]) + + def test_default_user_agent(self): + con = Urllib3HttpConnection() + self.assertEqual( + con._get_default_user_agent(), + "opensearch-py/%s (Python %s)" % (__versionstr__, python_version()), + ) + + def test_timeout_set(self): + con = Urllib3HttpConnection(timeout=42) + self.assertEqual(42, con.timeout) + + def test_keep_alive_is_on_by_default(self): + con = Urllib3HttpConnection() + self.assertEqual( + { + "connection": "keep-alive", + "content-type": "application/json", + "user-agent": con._get_default_user_agent(), + }, + con.headers, + ) + + def test_http_auth(self): + con = Urllib3HttpConnection(http_auth="username:secret") + self.assertEqual( + { + "authorization": "Basic dXNlcm5hbWU6c2VjcmV0", + "connection": "keep-alive", + "content-type": "application/json", + "user-agent": con._get_default_user_agent(), + }, + con.headers, + ) + + def test_http_auth_tuple(self): + con = Urllib3HttpConnection(http_auth=("username", "secret")) + self.assertEqual( + { + "authorization": "Basic dXNlcm5hbWU6c2VjcmV0", + "content-type": "application/json", + "connection": "keep-alive", + "user-agent": con._get_default_user_agent(), + }, + con.headers, + ) + + def test_http_auth_list(self): + con = Urllib3HttpConnection(http_auth=["username", "secret"]) + self.assertEqual( + { + "authorization": "Basic dXNlcm5hbWU6c2VjcmV0", + "content-type": "application/json", + "connection": "keep-alive", + "user-agent": con._get_default_user_agent(), + }, + con.headers, + ) + + @pytest.mark.skipif( + sys.version_info < (3, 6), reason="Urllib3AWSV4SignerAuth requires python3.6+" + ) + def test_aws_signer_as_http_auth(self): + region = "us-west-2" + + from opensearchpy.helpers.signer import Urllib3AWSV4SignerAuth + + auth = Urllib3AWSV4SignerAuth(self.mock_session(), region) + headers = auth("GET", "http://localhost", None) + self.assertIn("Authorization", headers) + self.assertIn("X-Amz-Date", headers) + self.assertIn("X-Amz-Security-Token", headers) + self.assertIn("X-Amz-Content-SHA256", headers) + + @pytest.mark.skipif( + sys.version_info < (3, 6), reason="Urllib3AWSV4SignerAuth requires python3.6+" + ) + def test_aws_signer_when_region_is_null(self): + session = self.mock_session() + + from opensearchpy.helpers.signer import Urllib3AWSV4SignerAuth + + with pytest.raises(ValueError) as e: + Urllib3AWSV4SignerAuth(session, None) + assert str(e.value) == "Region cannot be empty" + + with pytest.raises(ValueError) as e: + Urllib3AWSV4SignerAuth(session, "") + assert str(e.value) == "Region cannot be empty" + + @pytest.mark.skipif( + sys.version_info < (3, 6), reason="Urllib3AWSV4SignerAuth requires python3.6+" + ) + def test_aws_signer_when_credentials_is_null(self): + region = "us-west-1" + + from opensearchpy.helpers.signer import Urllib3AWSV4SignerAuth + + with pytest.raises(ValueError) as e: + Urllib3AWSV4SignerAuth(None, region) + assert str(e.value) == "Credentials cannot be empty" + + with pytest.raises(ValueError) as e: + Urllib3AWSV4SignerAuth("", region) + assert str(e.value) == "Credentials cannot be empty" + + @pytest.mark.skipif( + sys.version_info < (3, 6), reason="Urllib3AWSV4SignerAuth requires python3.6+" + ) + def test_aws_signer_when_service_is_specified(self): + region = "us-west-1" + service = "aoss" + + from opensearchpy.helpers.signer import Urllib3AWSV4SignerAuth + + auth = Urllib3AWSV4SignerAuth(self.mock_session(), region, service) + headers = auth("GET", "http://localhost", None) + self.assertIn("Authorization", headers) + self.assertIn("X-Amz-Date", headers) + self.assertIn("X-Amz-Security-Token", headers) + + def mock_session(self): + access_key = uuid.uuid4().hex + secret_key = uuid.uuid4().hex + token = uuid.uuid4().hex + dummy_session = Mock() + dummy_session.access_key = access_key + dummy_session.secret_key = secret_key + dummy_session.token = token + del dummy_session.get_frozen_credentials + + return dummy_session + + def test_uses_https_if_verify_certs_is_off(self): + with warnings.catch_warnings(record=True) as w: + con = Urllib3HttpConnection(use_ssl=True, verify_certs=False) + self.assertEqual(1, len(w)) + self.assertEqual( + "Connecting to https://localhost:9200 using SSL with verify_certs=False is insecure.", + str(w[0].message), + ) + + self.assertIsInstance(con.pool, urllib3.HTTPSConnectionPool) + + def test_nowarn_when_uses_https_if_verify_certs_is_off(self): + with warnings.catch_warnings(record=True) as w: + con = Urllib3HttpConnection( + use_ssl=True, verify_certs=False, ssl_show_warn=False + ) + self.assertEqual(0, len(w)) + + self.assertIsInstance(con.pool, urllib3.HTTPSConnectionPool) + + def test_doesnt_use_https_if_not_specified(self): + con = Urllib3HttpConnection() + self.assertIsInstance(con.pool, urllib3.HTTPConnectionPool) + + def test_no_warning_when_using_ssl_context(self): + ctx = ssl.create_default_context() + with warnings.catch_warnings(record=True) as w: + Urllib3HttpConnection(ssl_context=ctx) + self.assertEqual(0, len(w)) + + def test_warns_if_using_non_default_ssl_kwargs_with_ssl_context(self): + for kwargs in ( + {"ssl_show_warn": False}, + {"ssl_show_warn": True}, + {"verify_certs": True}, + {"verify_certs": False}, + {"ca_certs": "/path/to/certs"}, + {"ssl_show_warn": True, "ca_certs": "/path/to/certs"}, + ): + kwargs["ssl_context"] = ssl.create_default_context() + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + + Urllib3HttpConnection(**kwargs) + + self.assertEqual(1, len(w)) + self.assertEqual( + "When using `ssl_context`, all other SSL related kwargs are ignored", + str(w[0].message), + ) + + def test_uses_given_ca_certs(self): + path = "/path/to/my/ca_certs.pem" + c = Urllib3HttpConnection(use_ssl=True, ca_certs=path) + self.assertEqual(path, c.pool.ca_certs) + + def test_uses_default_ca_certs(self): + c = Urllib3HttpConnection(use_ssl=True) + self.assertEqual(Connection.default_ca_certs(), c.pool.ca_certs) + + def test_uses_no_ca_certs(self): + c = Urllib3HttpConnection(use_ssl=True, verify_certs=False) + self.assertIsNone(c.pool.ca_certs) + + @patch("opensearchpy.connection.base.logger") + def test_uncompressed_body_logged(self, logger): + con = self._get_mock_connection(connection_params={"http_compress": True}) + con.perform_request("GET", "/", body=b'{"example": "body"}') + + self.assertEqual(2, logger.debug.call_count) + req, resp = logger.debug.call_args_list + + self.assertEqual('> {"example": "body"}', req[0][0] % req[0][1:]) + self.assertEqual("< {}", resp[0][0] % resp[0][1:]) + + def test_surrogatepass_into_bytes(self): + buf = b"\xe4\xbd\xa0\xe5\xa5\xbd\xed\xa9\xaa" + con = self._get_mock_connection(response_body=buf) + status, headers, data = con.perform_request("GET", "/") + self.assertEqual(u"你好\uda6a", data) # fmt: skip + + @pytest.mark.skipif( + not reraise_exceptions, reason="RecursionError isn't defined in Python <3.5" + ) + def test_recursion_error_reraised(self): + conn = Urllib3HttpConnection() + + def urlopen_raise(*_, **__): + raise RecursionError("Wasn't modified!") + + conn.pool.urlopen = urlopen_raise + + with pytest.raises(RecursionError) as e: + conn.perform_request("GET", "/") + assert str(e.value) == "Wasn't modified!" + + +class TestSignerWithFrozenCredentials(TestUrllib3HttpConnection): + def mock_session(self): + access_key = uuid.uuid4().hex + secret_key = uuid.uuid4().hex + token = uuid.uuid4().hex + dummy_session = Mock() + dummy_session.access_key = access_key + dummy_session.secret_key = secret_key + dummy_session.token = token + dummy_session.get_frozen_credentials = Mock(return_value=dummy_session) + + return dummy_session + + @pytest.mark.skipif( + sys.version_info < (3, 6), reason="Urllib3AWSV4SignerAuth requires python3.6+" + ) + def test_urllib3_http_connection_aws_signer_frozen_credentials_as_http_auth(self): + region = "us-west-2" + + from opensearchpy.helpers.signer import Urllib3AWSV4SignerAuth + + mock_session = self.mock_session() + + auth = Urllib3AWSV4SignerAuth(mock_session, region) + headers = auth("GET", "http://localhost", None) + self.assertIn("Authorization", headers) + self.assertIn("X-Amz-Date", headers) + self.assertIn("X-Amz-Security-Token", headers) + self.assertIn("X-Amz-Content-SHA256", headers) + mock_session.get_frozen_credentials.assert_called_once() From ee4517d380a0cdfe2b419e32917630f549a63786 Mon Sep 17 00:00:00 2001 From: dblock Date: Mon, 23 Oct 2023 12:28:26 -0400 Subject: [PATCH 09/13] Rename AWSV4Signer. Signed-off-by: dblock --- opensearchpy/helpers/signer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/opensearchpy/helpers/signer.py b/opensearchpy/helpers/signer.py index 6304f805..b36b81e8 100644 --- a/opensearchpy/helpers/signer.py +++ b/opensearchpy/helpers/signer.py @@ -44,7 +44,7 @@ def fetch_url(prepared_request): # type: ignore return url.scheme + "://" + location + path + querystring -class AuthSigner: +class AWSV4Signer: """ Generic AWS V4 Request Signer. """ @@ -107,7 +107,7 @@ class RequestsAWSV4SignerAuth(requests.auth.AuthBase): """ def __init__(self, credentials, region, service="es"): # type: ignore - self.signer = AuthSigner(credentials, region, service) + self.signer = AWSV4Signer(credentials, region, service) def __call__(self, request): # type: ignore return self._sign_request(request) # type: ignore @@ -137,7 +137,7 @@ class AWSV4SignerAuth(RequestsAWSV4SignerAuth): class Urllib3AWSV4SignerAuth(Callable): # type: ignore def __init__(self, credentials, region, service="es"): # type: ignore - self.signer = AuthSigner(credentials, region, service) + self.signer = AWSV4Signer(credentials, region, service) def __call__(self, method: str, url: str, body: Any) -> Dict[str, str]: return self.signer.sign(method, url, body) From cb768749be020ca3882815d73464bb5bf5dee6a7 Mon Sep 17 00:00:00 2001 From: dblock Date: Mon, 23 Oct 2023 14:50:26 -0400 Subject: [PATCH 10/13] Clarified documentation of when to use Urllib3AWSV4SignerAuth vs. RequestHttpConnection. Signed-off-by: dblock --- guides/auth.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/guides/auth.md b/guides/auth.md index 215d47f0..a07d3996 100644 --- a/guides/auth.md +++ b/guides/auth.md @@ -1,5 +1,6 @@ - [Authentication](#authentication) - [IAM Authentication](#iam-authentication) + - [IAM Authentication with a Synchronous Client](#iam-authentication-with-a-synchronous-client) - [IAM Authentication with an Async Client](#iam-authentication-with-an-async-client) - [Kerberos](#kerberos) @@ -9,7 +10,11 @@ OpenSearch allows you to use different methods for the authentication via `conne ## IAM Authentication -Opensearch-py supports IAM-based authentication via `RequestsAWSV4SignerAuth` and `Urllib3AWSV4SignerAuth`, which use `RequestHttpConnection` and `Urllib3HttpConnection` respectively, as the transport classes for communicating with OpenSearch clusters running in Amazon Managed OpenSearch and OpenSearch Serverless, and works in conjunction with [botocore](https://pypi.org/project/botocore/). +This library supports IAM-based authentication when communicating with OpenSearch clusters running in Amazon Managed OpenSearch and OpenSearch Serverless. + +## IAM Authentication with a Synchronous Client + +For `Urllib3HttpConnection` use `Urllib3AWSV4SignerAuth`, and for `RequestHttpConnection` use `RequestsAWSV4SignerAuth`. ```python from opensearchpy import OpenSearch, Urllib3HttpConnection, Urllib3AWSV4SignerAuth From 9e6b9aa07d32dd892208177102335b4bd75acd67 Mon Sep 17 00:00:00 2001 From: dblock Date: Mon, 23 Oct 2023 14:57:12 -0400 Subject: [PATCH 11/13] Move fetch_url inside the signer class. Signed-off-by: dblock --- opensearchpy/helpers/signer.py | 53 +++++++++++++++++----------------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/opensearchpy/helpers/signer.py b/opensearchpy/helpers/signer.py index b36b81e8..436909e7 100644 --- a/opensearchpy/helpers/signer.py +++ b/opensearchpy/helpers/signer.py @@ -18,32 +18,6 @@ from urllib.parse import parse_qs, urlencode, urlparse -def fetch_url(prepared_request): # type: ignore - """ - This is a util method that helps in reconstructing the request url. - :param prepared_request: unsigned request - :return: reconstructed url - """ - url = urlparse(prepared_request.url) - path = url.path or "/" - - # fetch the query string if present in the request - querystring = "" - if url.query: - querystring = "?" + urlencode( - parse_qs(url.query, keep_blank_values=True), doseq=True - ) - - # fetch the host information from headers - headers = dict( - (key.lower(), value) for key, value in prepared_request.headers.items() - ) - location = headers.get("host") or url.netloc - - # construct the url and return - return url.scheme + "://" + location + path + querystring - - class AWSV4Signer: """ Generic AWS V4 Request Signer. @@ -122,13 +96,38 @@ def _sign_request(self, prepared_request): # type: ignore prepared_request.headers.update( self.signer.sign( prepared_request.method, - fetch_url(prepared_request), # type: ignore + self._fetch_url(prepared_request), # type: ignore prepared_request.body, ) ) return prepared_request + def _fetch_url(self, prepared_request): # type: ignore + """ + This is a util method that helps in reconstructing the request url. + :param prepared_request: unsigned request + :return: reconstructed url + """ + url = urlparse(prepared_request.url) + path = url.path or "/" + + # fetch the query string if present in the request + querystring = "" + if url.query: + querystring = "?" + urlencode( + parse_qs(url.query, keep_blank_values=True), doseq=True + ) + + # fetch the host information from headers + headers = dict( + (key.lower(), value) for key, value in prepared_request.headers.items() + ) + location = headers.get("host") or url.netloc + + # construct the url and return + return url.scheme + "://" + location + path + querystring + # Deprecated: use RequestsAWSV4SignerAuth class AWSV4SignerAuth(RequestsAWSV4SignerAuth): From e916060866ed70125e7607099e826efa9110f2d9 Mon Sep 17 00:00:00 2001 From: dblock Date: Mon, 23 Oct 2023 15:42:52 -0400 Subject: [PATCH 12/13] Added unit test for Urllib3AWSV4SignerAuth adding headers. Signed-off-by: dblock --- .../test_urllib3_http_connection.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test_opensearchpy/test_connection/test_urllib3_http_connection.py b/test_opensearchpy/test_connection/test_urllib3_http_connection.py index b3ae0bca..854e22e4 100644 --- a/test_opensearchpy/test_connection/test_urllib3_http_connection.py +++ b/test_opensearchpy/test_connection/test_urllib3_http_connection.py @@ -179,6 +179,29 @@ def test_http_auth_list(self): con.headers, ) + @pytest.mark.skipif( + sys.version_info < (3, 6), reason="Urllib3AWSV4SignerAuth requires python3.6+" + ) + @patch( + "urllib3.HTTPConnectionPool.urlopen", + return_value=Mock(status=200, headers=HTTPHeaderDict({}), data=b"{}"), + ) + def test_aws_signer_as_http_auth_adds_headers(self, mock_open): + from opensearchpy.helpers.signer import Urllib3AWSV4SignerAuth + + auth = Urllib3AWSV4SignerAuth(self.mock_session(), "us-west-2") + con = Urllib3HttpConnection(http_auth=auth, headers={"x": "y"}) + con.perform_request("GET", "/") + self.assertEqual(mock_open.call_count, 1) + headers = mock_open.call_args[1]["headers"] + self.assertEqual(headers["x"], "y") + self.assertTrue( + headers["Authorization"].startswith("AWS4-HMAC-SHA256 Credential=") + ) + self.assertIn("X-Amz-Date", headers) + self.assertIn("X-Amz-Security-Token", headers) + self.assertIn("X-Amz-Content-SHA256", headers) + @pytest.mark.skipif( sys.version_info < (3, 6), reason="Urllib3AWSV4SignerAuth requires python3.6+" ) From 284a5fd34f811088ffd9c5ad80b0b8d07968b29c Mon Sep 17 00:00:00 2001 From: dblock Date: Mon, 23 Oct 2023 15:57:10 -0400 Subject: [PATCH 13/13] Added unit test for signing to include query string. Signed-off-by: dblock --- .../test_requests_http_connection.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test_opensearchpy/test_connection/test_requests_http_connection.py b/test_opensearchpy/test_connection/test_requests_http_connection.py index 0b552ee0..db426fa5 100644 --- a/test_opensearchpy/test_connection/test_requests_http_connection.py +++ b/test_opensearchpy/test_connection/test_requests_http_connection.py @@ -460,6 +460,29 @@ def test_aws_signer_when_service_is_specified(self): self.assertIn("X-Amz-Date", prepared_request.headers) self.assertIn("X-Amz-Security-Token", prepared_request.headers) + @pytest.mark.skipif( + sys.version_info < (3, 6), reason="RequestsAWSV4SignerAuth requires python3.6+" + ) + @patch("opensearchpy.helpers.signer.AWSV4Signer.sign") + def test_aws_signer_signs_with_query_string(self, mock_sign): + region = "us-west-1" + service = "aoss" + + import requests + + from opensearchpy.helpers.signer import RequestsAWSV4SignerAuth + + auth = RequestsAWSV4SignerAuth(self.mock_session(), region, service) + prepared_request = requests.Request( + "GET", "http://localhost", params={"key1": "value1", "key2": "value2"} + ).prepare() + auth(prepared_request) + self.assertEqual(mock_sign.call_count, 1) + self.assertEqual( + mock_sign.call_args[0], + ("GET", "http://localhost/?key1=value1&key2=value2", None), + ) + @pytest.mark.skipif( sys.version_info < (3, 0),