diff --git a/.ci/test-matrix.yml b/.ci/test-matrix.yml index c76a1bff..d63b5793 100755 --- a/.ci/test-matrix.yml +++ b/.ci/test-matrix.yml @@ -2,9 +2,6 @@ TEST_SUITE: - oss PYTHON_VERSION: - - "2.7" - - "3.4" - - "3.5" - "3.6" - "3.7" - "3.8" diff --git a/CHANGELOG.md b/CHANGELOG.md index 41524e8c..d2ccc9b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,12 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ### Added - Added generating imports and headers to API generator ([#467](https://github.com/opensearch-project/opensearch-py/pull/467)) - Added point-in-time APIs (create_pit, delete_pit, delete_all_pits, get_all_pits) and Security Client APIs (health and update_audit_configuration) ([#502](https://github.com/opensearch-project/opensearch-py/pull/502)) -- Added new guide for using index templates with the client ([#531](https://github.com/opensearch-project/opensearch-py/pull/531)) +- Added guide on using index templates ([#531](https://github.com/opensearch-project/opensearch-py/pull/531)) - 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 guide on the document lifecycle API(s) ([#545](https://github.com/opensearch-project/opensearch-py/pull/545)) +- 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)) @@ -18,6 +21,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ### Deprecated - Deprecated point-in-time APIs (list_all_point_in_time, create_point_in_time, delete_point_in_time) and Security Client APIs (health_check and update_audit_config) ([#502](https://github.com/opensearch-project/opensearch-py/pull/502)) ### Removed +- Removed leftover support for Python 2.7 ([#548](https://github.com/opensearch-project/opensearch-py/pull/548)) ### Fixed ### Security ### Dependencies diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index ec39602f..5fe9cad1 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 @@ -76,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/USER_GUIDE.md b/USER_GUIDE.md index a56c1235..2815d14a 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -35,6 +35,8 @@ In general, we recommend using a package manager, such as [poetry](https://pytho In the example below, we create a client, create an index with non-default settings, insert a document into the index, search for the document, delete the document, and finally delete the index. +You can more find information on the full document lifecycle in [guides/document_lifecycle.md](guides/document_lifecycle.md). + You can find working versions of the code below that can be run with a local instance of OpenSearch in [samples](samples). ### Creating a Client @@ -155,6 +157,7 @@ print(response) - [Using a Proxy](guides/proxy.md) - [Index Templates](guides/index_template.md) - [Advanced Index Actions](guides/advanced_index_actions.md) +- [Making Raw JSON REST Requests](guides/json.md) - [Connection Classes](guides/connection_classes.md) ## Plugins diff --git a/dev-requirements.txt b/dev-requirements.txt index 76e1acdb..04cfb3e8 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -12,14 +12,13 @@ pytz numpy; python_version<"3.10" pandas; python_version<"3.10" -pyyaml>=5.4; python_version>="3.6" -pyyaml==5.3.1; python_version<"3.6" +pyyaml>=5.4 isort -black; python_version>="3.6" +black twine # Requirements for testing [async] extra -aiohttp; python_version>="3.6" -pytest-asyncio<=0.21.1; python_version>="3.6" -unasync; python_version>="3.6" +aiohttp +pytest-asyncio<=0.21.1 +unasync diff --git a/guides/auth.md b/guides/auth.md index 3e7f4092..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,24 +10,28 @@ 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/). +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, 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/guides/document_lifecycle.md b/guides/document_lifecycle.md new file mode 100644 index 00000000..99d9d72f --- /dev/null +++ b/guides/document_lifecycle.md @@ -0,0 +1,182 @@ +# Document Lifecycle Guide +- [Document Lifecycle](#document-lifecycle) + - [Setup](#setup) + - [Document API Actions](#document-api-actions) + - [Create a new document with specified ID](#create-a-new-document-with-specified-id) + - [Create a new document with auto-generated ID](#create-a-new-document-with-auto-generated-id) + - [Get a document](#get-a-document) + - [Get multiple documents](#get-multiple-documents) + - [Check if a document exists](#check-if-a-document-exists) + - [Update a document](#update-a-document) + - [Update multiple documents by query](#update-multiple-documents-by-query) + - [Delete a document](#delete-a-document) + - [Delete multiple documents by query](#delete-multiple-documents-by-query) + - [Cleanup](#cleanup) + + +# Document Lifecycle +This guide covers OpenSearch Python Client API actions for Document Lifecycle. You'll learn how to create, read, update, and delete documents in your OpenSearch cluster. Whether you're new to OpenSearch or an experienced user, this guide provides the information you need to manage your document lifecycle effectively. + +## Setup +Assuming you have OpenSearch running locally on port 9200, you can create a client instance +with the following code: + +```python +from opensearchpy import OpenSearch +client = OpenSearch( + hosts=['https://localhost:9200'], + use_ssl=True, + verify_certs=False, + http_auth=('admin', 'admin') +) +``` + +Next, create an index named `movies` with the default settings: + +```python +index = 'movies' +if not client.indices.exists(index=index): + client.indices.create(index=index) +``` + +## Document API Actions + +### Create a new document with specified ID +To create a new document, use the `create` or `index` API action. The following code creates two new documents with IDs of `1` and `2`: + +```python +client.create(index=index, id=1, body={'title': 'Beauty and the Beast', 'year': 1991}) +client.create(index=index, id=2, body={'title': 'Beauty and the Beast - Live Action', 'year': 2017}) +``` + +Note that the `create` action is NOT idempotent. If you try to create a document with an ID that already exists, the request will fail: + +```python +try: + client.create(index=index, id=1, body={'title': 'Just Another Movie'}) +except Exception as e: + print(e) +``` + +The `index` action, on the other hand, is idempotent. If you try to index a document with an existing ID, the request will succeed and overwrite the existing document. Note that no new document will be created in this case. You can think of the `index` action as an upsert: + +```python +client.index(index=index, id=2, body={'title': 'Updated Title'}) +client.index(index=index, id=2, body={'title': 'The Lion King', 'year': 1994}) +``` + +### Create a new document with auto-generated ID +You can also create a new document with an auto-generated ID by omitting the `id` parameter. The following code creates documents with an auto-generated IDs in the `movies` index: + +```python +OR client.index(index=index, body={"title": "The Lion King 2", "year": 1998}) +``` + +In this case, the ID of the created document in the `result` field of the response body: + +```python +{ + "_index": "movies", + "_type": "_doc", + "_id": "1", + "_version": 1, + "result": "created", + "_shards": { + "total": 2, + "successful": 1, + "failed": 0 + }, + "_seq_no": 0, + "_primary_term": 1 +} +``` + +### Get a document +To get a document, use the `get` API action. The following code gets the document with ID `1` from the `movies` index: + +```python +client.get(index=index, id=1)['_source'] +# OUTPUT: {"title"=>"Beauty and the Beast","year"=>1991} +``` + +You can also use `_source_includes` and `_source_excludes` parameters to specify which fields to include or exclude in the response: + +```python +client.get(index=index, id=1, _source_includes=['title'])['_source'] +# OUTPUT: {"title": "Beauty and the Beast"} + +client.get(index=index, id=1, _source_excludes=['title'])['_source'] +# OUTPUT: {"year": 1991} +``` + +### Get multiple documents +To get multiple documents, use the `mget` API action: + +```python +client.mget(index=index, body={ 'docs': [{ '_id': 1 }, { '_id': 2 }] })['docs'] +``` + +### Check if a document exists +To check if a document exists, use the `exists` API action. The following code checks if the document with ID `1` exists in the `movies` index: + +```python +client.exists(index=index, id=1) +``` + +### Update a document +To update a document, use the `update` API action. The following code updates the `year` field of the document with ID `1` in the `movies` index: + +```python +client.update(index=index, id=1, body={'doc': {'year': 1995}}) +``` + +Alternatively, you can use the `script` parameter to update a document using a script. The following code increments the `year` field of the of document with ID `1` by 5 using painless script, the default scripting language in OpenSearch: + +```python +client.update(index=index, id=1, body={ 'script': { 'source': 'ctx._source.year += 5' } }) +``` + +Note that while both `update` and `index` actions perform updates, they are not the same. The `update` action is a partial update, while the `index` action is a full update. The `update` action only updates the fields that are specified in the request body, while the `index` action overwrites the entire document with the new document. + +### Update multiple documents by query + +To update documents that match a query, use the `update_by_query` API action. The following code decreases the `year` field of all documents with `year` greater than 2023: + +```python +client.update_by_query(index=index, body={ + 'script': { 'source': 'ctx._source.year -= 1' }, + 'query': { 'range': { 'year': { 'gt': 2023 } } } +}) +``` + +### Delete a document +To delete a document, use the `delete` API action. The following code deletes the document with ID `1`: + +```python +client.delete(index=index, id=1) +``` + +By default, the `delete` action is not idempotent. If you try to delete a document that does not exist, or delete the same document twice, you will run into Not Found (404) error. You can make the `delete` action idempotent by setting the `ignore` parameter to `404`: + +```python +client.delete(index=index, id=1, ignore=404) +``` + +### Delete multiple documents by query +To delete documents that match a query, use the `delete_by_query` API action. The following code deletes all documents with `year` greater than 2023: + +```python +client.delete_by_query(index=index, body={ + 'query': { 'range': { 'year': { 'gt': 2023 } } } +}) +``` + +## Cleanup +To clean up the resources created in this guide, delete the `movies` index: + +```python +client.indices.delete(index=index) +``` + +# Sample Code +See [document_lifecycle_sample.py](/samples/document_lifecycle/document_lifecycle_sample.py) for a working sample of the concepts in this guide. \ No newline at end of file diff --git a/guides/json.md b/guides/json.md new file mode 100644 index 00000000..edefa209 --- /dev/null +++ b/guides/json.md @@ -0,0 +1,66 @@ +- [Making Raw JSON REST Requests](#making-raw-json-rest-requests) + - [GET](#get) + - [PUT](#put) + - [POST](#post) + - [DELETE](#delete) + +# Making Raw JSON REST Requests + +The OpenSearch client implements many high-level REST DSLs that invoke OpenSearch APIs. However you may find yourself in a situation that requires you to invoke an API that is not supported by the client. Use `client.transport.perform_request` to do so. See [samples/json](../samples/json) for a complete working sample. + +## GET + +The following example returns the server version information via `GET /`. + +```python +info = client.transport.perform_request('GET', '/') +print(f"Welcome to {info['version']['distribution']} {info['version']['number']}!") +``` + +Note that the client will parse the response as JSON when appropriate. + +## PUT + +The following example creates an index. + +```python +index_body = { + 'settings': { + 'index': { + 'number_of_shards': 4 + } + } +} + +client.transport.perform_request("PUT", "/movies", body=index_body) +``` + +Note that the client will raise errors automatically. For example, if the index already exists, an `opensearchpy.exceptions.RequestError: RequestError(400, 'resource_already_exists_exception',` will be thrown. + +## POST + +The following example searches for a document. + +```python +q = 'miller' + +query = { + 'size': 5, + 'query': { + 'multi_match': { + 'query': q, + 'fields': ['title^2', 'director'] + } + } +} + +client.transport.perform_request("POST", "/movies/_search", body = query) +``` + +## DELETE + +The following example deletes an index. + +```python +client.transport.perform_request("DELETE", "/movies") +``` diff --git a/noxfile.py b/noxfile.py index 80b4e400..6b734b48 100644 --- a/noxfile.py +++ b/noxfile.py @@ -36,7 +36,7 @@ ) -@nox.session(python=["2.7", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11"]) +@nox.session(python=["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"]) def test(session): session.install(".") session.install("-r", "dev-requirements.txt") diff --git a/opensearchpy/__init__.py b/opensearchpy/__init__.py index 6669f179..a0ea9f60 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", @@ -241,22 +248,15 @@ "tokenizer", ] -try: - # Asyncio only supported on Python 3.6+ - if sys.version_info < (3, 6): - raise ImportError - - from ._async.client import AsyncOpenSearch - from ._async.http_aiohttp import AIOHttpConnection, AsyncConnection - from ._async.transport import AsyncTransport - from .connection import AsyncHttpConnection +from ._async.client import AsyncOpenSearch +from ._async.http_aiohttp import AIOHttpConnection, AsyncConnection +from ._async.transport import AsyncTransport +from .connection import AsyncHttpConnection - __all__ += [ - "AIOHttpConnection", - "AsyncConnection", - "AsyncTransport", - "AsyncOpenSearch", - "AsyncHttpConnection", - ] -except (ImportError, SyntaxError): - pass +__all__ += [ + "AIOHttpConnection", + "AsyncConnection", + "AsyncTransport", + "AsyncOpenSearch", + "AsyncHttpConnection", +] diff --git a/opensearchpy/__init__.pyi b/opensearchpy/__init__.pyi index 01fccaec..0fa4afcf 100644 --- a/opensearchpy/__init__.pyi +++ b/opensearchpy/__init__.pyi @@ -27,6 +27,10 @@ import sys from typing import Tuple +from ._async.client import AsyncOpenSearch as AsyncOpenSearch +from ._async.http_aiohttp import AIOHttpConnection as AIOHttpConnection +from ._async.http_aiohttp import AsyncConnection as AsyncConnection +from ._async.transport import AsyncTransport as AsyncTransport from .client import OpenSearch as OpenSearch from .connection import AsyncHttpConnection as AsyncHttpConnection from .connection import Connection as Connection @@ -54,6 +58,8 @@ from .exceptions import SSLError as SSLError from .exceptions import TransportError as TransportError from .exceptions import UnknownDslObject as UnknownDslObject from .exceptions import ValidationException as ValidationException +from .helpers import AWSV4SignerAsyncAuth as AWSV4SignerAsyncAuth +from .helpers import AWSV4SignerAuth as AWSV4SignerAuth from .helpers.aggs import A as A from .helpers.analysis import Analyzer, CharFilter, Normalizer, TokenFilter, Tokenizer from .helpers.document import Document as Document @@ -120,19 +126,6 @@ from .helpers.wrappers import Range as Range from .serializer import JSONSerializer as JSONSerializer from .transport import Transport as Transport -try: - if sys.version_info < (3, 6): - raise ImportError - - from ._async.client import AsyncOpenSearch as AsyncOpenSearch - from ._async.http_aiohttp import AIOHttpConnection as AIOHttpConnection - from ._async.http_aiohttp import AsyncConnection as AsyncConnection - from ._async.transport import AsyncTransport as AsyncTransport - from .helpers import AWSV4SignerAsyncAuth as AWSV4SignerAsyncAuth - from .helpers import AWSV4SignerAuth as AWSV4SignerAuth -except (ImportError, SyntaxError): - pass - VERSION: Tuple[int, int, int] __version__: Tuple[int, int, int] __versionstr__: str diff --git a/opensearchpy/connection/__init__.py b/opensearchpy/connection/__init__.py index 1b9ad2cd..6e331a54 100644 --- a/opensearchpy/connection/__init__.py +++ b/opensearchpy/connection/__init__.py @@ -25,9 +25,8 @@ # under the License. -import sys - from .base import Connection +from .http_async import AsyncHttpConnection from .http_requests import RequestsHttpConnection from .http_urllib3 import Urllib3HttpConnection, create_ssl_context @@ -36,17 +35,5 @@ "RequestsHttpConnection", "Urllib3HttpConnection", "create_ssl_context", + "AsyncHttpConnection", ] - -try: - # Asyncio only supported on Python 3.6+ - if sys.version_info < (3, 6): - raise ImportError - - from .http_async import AsyncHttpConnection - - __all__ += [ - "AsyncHttpConnection", - ] -except (ImportError, SyntaxError): - pass diff --git a/opensearchpy/connection/http_urllib3.py b/opensearchpy/connection/http_urllib3.py index 4bc27bbb..0042cc3c 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,10 +129,17 @@ 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)) + + 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)) pool_class = urllib3.HTTPConnectionPool kw = {} @@ -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, 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..8057de7e 100644 --- a/opensearchpy/helpers/__init__.py +++ b/opensearchpy/helpers/__init__.py @@ -25,8 +25,12 @@ # under the License. -import sys - +from .._async.helpers.actions import ( + async_bulk, + async_reindex, + async_scan, + async_streaming_bulk, +) from .actions import ( _chunk_actions, _process_bulk_chunk, @@ -39,7 +43,7 @@ ) from .asyncsigner import AWSV4SignerAsyncAuth from .errors import BulkIndexError, ScanError -from .signer import AWSV4SignerAuth +from .signer import AWSV4SignerAuth, RequestsAWSV4SignerAuth, Urllib3AWSV4SignerAuth __all__ = [ "BulkIndexError", @@ -54,16 +58,10 @@ "_process_bulk_chunk", "AWSV4SignerAuth", "AWSV4SignerAsyncAuth", + "RequestsAWSV4SignerAuth", + "Urllib3AWSV4SignerAuth", + "async_scan", + "async_bulk", + "async_reindex", + "async_streaming_bulk", ] - - -# Asyncio only supported on Python 3.6+ -if sys.version_info >= (3, 6): - from .._async.helpers.actions import ( - async_bulk, - async_reindex, - async_scan, - async_streaming_bulk, - ) - - __all__ += ["async_scan", "async_bulk", "async_reindex", "async_streaming_bulk"] diff --git a/opensearchpy/helpers/__init__.pyi b/opensearchpy/helpers/__init__.pyi index 59b5cefd..01d4973c 100644 --- a/opensearchpy/helpers/__init__.pyi +++ b/opensearchpy/helpers/__init__.pyi @@ -26,6 +26,10 @@ import sys +from .._async.helpers.actions import async_bulk as async_bulk +from .._async.helpers.actions import async_reindex as async_reindex +from .._async.helpers.actions import async_scan as async_scan +from .._async.helpers.actions import async_streaming_bulk as async_streaming_bulk from .actions import _chunk_actions as _chunk_actions from .actions import _process_bulk_chunk as _process_bulk_chunk from .actions import bulk as bulk @@ -34,19 +38,8 @@ from .actions import parallel_bulk as parallel_bulk from .actions import reindex as reindex from .actions import scan as scan from .actions import streaming_bulk as streaming_bulk +from .asyncsigner import AWSV4SignerAsyncAuth as AWSV4SignerAsyncAuth from .errors import BulkIndexError as BulkIndexError from .errors import ScanError as ScanError - -try: - # Asyncio only supported on Python 3.6+ - if sys.version_info < (3, 6): - raise ImportError - - from .._async.helpers.actions import async_bulk as async_bulk - from .._async.helpers.actions import async_reindex as async_reindex - from .._async.helpers.actions import async_scan as async_scan - from .._async.helpers.actions import async_streaming_bulk as async_streaming_bulk - from .asyncsigner import AWSV4SignerAsyncAuth as AWSV4SignerAsyncAuth - from .signer import AWSV4SignerAuth as AWSV4SignerAuth -except (ImportError, SyntaxError): - pass +from .signer import AWSV4SignerAuth as AWSV4SignerAuth +from .signer import RequestsAWSV4SignerAuth, Urllib3AWSV4SignerAuth diff --git a/opensearchpy/helpers/signer.py b/opensearchpy/helpers/signer.py index 176f6ac9..436909e7 100644 --- a/opensearchpy/helpers/signer.py +++ b/opensearchpy/helpers/signer.py @@ -8,6 +8,7 @@ # GitHub history for details. import sys +from typing import Any, Callable, Dict import requests @@ -17,38 +18,12 @@ from urllib.parse import parse_qs, urlencode, urlparse -def fetch_url(prepared_request): # type: ignore +class AWSV4Signer: """ - This is a util method that helps in reconstructing the request url. - :param prepared_request: unsigned request - :return: reconstructed url + Generic AWS V4 Request Signer. """ - 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 AWSV4SignerAuth(requests.auth.AuthBase): - """ - AWS V4 Request Signer for Requests. - """ - - 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 @@ -61,27 +36,20 @@ 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_request(self, prepared_request): # type: ignore + def sign(self, method: str, url: str, body: Any) -> Dict[str, str]: """ - 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, @@ -101,9 +69,74 @@ 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 + headers = dict(aws_request.headers.items()) + headers["X-Amz-Content-SHA256"] = sig_v4_auth.payload(aws_request) + + return headers + + +class RequestsAWSV4SignerAuth(requests.auth.AuthBase): + """ + AWS V4 Request Signer for Requests. + """ + + def __init__(self, credentials, region, service="es"): # type: ignore + self.signer = AWSV4Signer(credentials, region, service) + + def __call__(self, request): # type: ignore + return self._sign_request(request) # 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 + """ + + prepared_request.headers.update( + self.signer.sign( + prepared_request.method, + 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): + pass + + +class Urllib3AWSV4SignerAuth(Callable): # type: ignore + def __init__(self, credentials, region, service="es"): # type: ignore + 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) diff --git a/samples/aws/README.md b/samples/aws/README.md new file mode 100644 index 00000000..17ad4ee0 --- /dev/null +++ b/samples/aws/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 aws/search-urllib.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/aws/search-requests.py b/samples/aws/search-requests.py new file mode 100644 index 00000000..1f14f55e --- /dev/null +++ b/samples/aws/search-requests.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 RequestsAWSV4SignerAuth, 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 = RequestsAWSV4SignerAuth(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/aws/search-urllib3.py b/samples/aws/search-urllib3.py new file mode 100644 index 00000000..46d6a89f --- /dev/null +++ b/samples/aws/search-urllib3.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 diff --git a/samples/document_lifecycle/document_lifecycle_sample.py b/samples/document_lifecycle/document_lifecycle_sample.py new file mode 100644 index 00000000..e93993aa --- /dev/null +++ b/samples/document_lifecycle/document_lifecycle_sample.py @@ -0,0 +1,77 @@ +from opensearchpy import OpenSearch + +# For cleaner output, comment in the two lines below to disable warnings and informational messages +# import urllib3 +# urllib3.disable_warnings() + + +# Connect to OpenSearch +client = OpenSearch( + hosts=['https://localhost:9200'], + use_ssl=True, + verify_certs=False, + http_auth=('admin', 'admin') +) + +# Create an index +index = 'movies' +if not client.indices.exists(index=index): + client.indices.create(index=index) + +# Create documents +client.index(index=index, id=1, body={'title': 'Beauty and the Beast', 'year': 1991}) +client.index(index=index, id=2, body={'title': 'Beauty and the Beast - Live Action', 'year': 2017}) + +# Index a document +client.index(index=index, id=2, body={'title': 'The Lion King', 'year': 1994}) + +# Create a document with auto-generated ID +result = client.index(index=index, body={'title': 'The Lion King 2', 'year': 1998}) +print(result) + +# Get a document +result = client.get(index=index, id=1)['_source'] +print(result) + +# Get a document with _source includes +result = client.get(index=index, id=1, _source_includes=['title'])['_source'] +print(result) + +# Get a document with _source excludes +result = client.get(index=index, id=1, _source_excludes=['title'])['_source'] +print(result) + +# Get multiple documents +result = client.mget(index=index, body={ 'docs': [{ '_id': 1 }, { '_id': 2 }] })['docs'] +print(result) + +# Check if a document exists +result = client.exists(index=index, id=1) +print(result) + +# Update a document +client.update(index=index, id=1, body={'doc': {'year': 1995}}) + +# Update a document using script +client.update(index=index, id=1, body={ 'script': { 'source': 'ctx._source.year += 5' } }) + +# Update multiple documents by query +client.update_by_query(index=index, body={ + 'script': { 'source': 'ctx._source.year -= 1' }, + 'query': { 'range': { 'year': { 'gt': 2023 } } } +}) + +# Delete a document +client.delete(index=index, id=1) + +# Delete a document with ignore 404 +client.delete(index=index, id=1, ignore=404) + +# Delete multiple documents by query +client.delete_by_query(index=index, body={ + 'query': { 'range': { 'year': { 'gt': 2023 } } } +}) + +# Delete the index +client.indices.delete(index=index) +print("Deleted index!") \ No newline at end of file diff --git a/samples/json/hello-async.py b/samples/json/hello-async.py new file mode 100755 index 00000000..aa4840c4 --- /dev/null +++ b/samples/json/hello-async.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python + +# 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. + +import asyncio + +from opensearchpy import AsyncOpenSearch + +async def main(): + # connect to OpenSearch + host = 'localhost' + port = 9200 + auth = ('admin', 'admin') # For testing only. Don't store credentials in code. + + client = AsyncOpenSearch( + hosts = [{'host': host, 'port': port}], + http_auth = auth, + use_ssl = True, + verify_certs = False, + ssl_show_warn = False + ) + + try: + info = await client.transport.perform_request('GET', '/') + print(f"Welcome to {info['version']['distribution']} {info['version']['number']}!") + + # create an index + + index_name = 'movies' + + index_body = { + 'settings': { + 'index': { + 'number_of_shards': 4 + } + } + } + + print(await client.transport.perform_request("PUT", f"/{index_name}", body=index_body)) + + # add a document to the index + + document = { + 'title': 'Moneyball', + 'director': 'Bennett Miller', + 'year': '2011' + } + + id = '1' + + print(await client.transport.perform_request("PUT", f"/{index_name}/_doc/{id}?refresh=true", body = document)) + + # search for a document + + q = 'miller' + + query = { + 'size': 5, + 'query': { + 'multi_match': { + 'query': q, + 'fields': ['title^2', 'director'] + } + } + } + + print(await client.transport.perform_request("POST", f"/{index_name}/_search", body = query)) + + # delete the document + + print(await client.transport.perform_request("DELETE", f"/{index_name}/_doc/{id}")) + + # delete the index + + print(await client.transport.perform_request("DELETE", f"/{index_name}")) + + + finally: + await client.close() + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.run_until_complete(main()) + loop.close() + diff --git a/samples/json/hello.py b/samples/json/hello.py new file mode 100755 index 00000000..d5b8e70f --- /dev/null +++ b/samples/json/hello.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python + +# 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. + +from opensearchpy import OpenSearch + +# connect to OpenSearch + +host = 'localhost' +port = 9200 +auth = ('admin', 'admin') # For testing only. Don't store credentials in code. + +client = OpenSearch( + hosts = [{'host': host, 'port': port}], + http_auth = auth, + use_ssl = True, + verify_certs = False, + ssl_show_warn = False +) + +info = client.transport.perform_request('GET', '/') +print(f"Welcome to {info['version']['distribution']} {info['version']['number']}!") + +# create an index + +index_name = 'movies' + +index_body = { + 'settings': { + 'index': { + 'number_of_shards': 4 + } + } +} + +print(client.transport.perform_request("PUT", f"/{index_name}", body=index_body)) + +# add a document to the index + +document = { + 'title': 'Moneyball', + 'director': 'Bennett Miller', + 'year': '2011' +} + +id = '1' + +print(client.transport.perform_request("PUT", f"/{index_name}/_doc/{id}?refresh=true", body = document)) + +# search for a document + +q = 'miller' + +query = { + 'size': 5, + 'query': { + 'multi_match': { + 'query': q, + 'fields': ['title^2', 'director'] + } + } +} + +print(client.transport.perform_request("POST", f"/{index_name}/_search", body = query)) + +# delete the document + +print(client.transport.perform_request("DELETE", f"/{index_name}/_doc/{id}")) + +# delete the index + +print(client.transport.perform_request("DELETE", f"/{index_name}")) 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/setup.py b/setup.py index 8bde5f40..f4163840 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,6 @@ import re -import sys from os.path import abspath, dirname, join from setuptools import find_packages, setup @@ -54,8 +53,7 @@ "requests>=2.4.0, <3.0.0", "six", "python-dateutil", - # ipaddress is included in stdlib since python 3.3 - 'ipaddress; python_version<"3.3"', + "certifi>=2022.12.07", ] tests_require = [ "requests>=2.0.0, <3.0.0", @@ -65,11 +63,9 @@ "pytest>=3.0.0", "pytest-cov", "pytz", - "botocore;python_version>='3.6'", + "botocore", + "pytest-mock<4.0.0", ] -if sys.version_info >= (3, 6): - tests_require.append("pytest-mock<4.0.0") - install_requires.append("certifi>=2022.12.07") async_require = ["aiohttp>=3,<4"] @@ -103,11 +99,6 @@ "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", diff --git a/test_opensearchpy/run_tests.py b/test_opensearchpy/run_tests.py index e0461af7..ca9db82a 100755 --- a/test_opensearchpy/run_tests.py +++ b/test_opensearchpy/run_tests.py @@ -116,27 +116,19 @@ def run_all(argv=None): if test_pattern: argv.append("-k %s" % test_pattern) else: - ignores = [] - # Python 3.6+ is required for async - if sys.version_info < (3, 6): - ignores.append("test_opensearchpy/test_async/") - - ignores.extend( - [ - "test_opensearchpy/test_server/", - "test_opensearchpy/test_server_secured/", - "test_opensearchpy/test_async/test_server/", - "test_opensearchpy/test_async/test_server_secured/", - ] - ) + ignores = [ + "test_opensearchpy/test_server/", + "test_opensearchpy/test_server_secured/", + "test_opensearchpy/test_async/test_server/", + "test_opensearchpy/test_async/test_server_secured/", + ] # Jenkins/Github actions, only run server tests if environ.get("TEST_TYPE") == "server": test_dir = abspath(dirname(__file__)) if secured: argv.append(join(test_dir, "test_server_secured")) - if sys.version_info >= (3, 6): - argv.append(join(test_dir, "test_async/test_server_secured")) + argv.append(join(test_dir, "test_async/test_server_secured")) ignores.extend( [ "test_opensearchpy/test_server/", @@ -145,8 +137,7 @@ def run_all(argv=None): ) else: argv.append(join(test_dir, "test_server")) - if sys.version_info >= (3, 6): - argv.append(join(test_dir, "test_async/test_server")) + argv.append(join(test_dir, "test_async/test_server")) ignores.extend( [ "test_opensearchpy/test_server_secured/", 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_async/test_signer.py b/test_opensearchpy/test_async/test_signer.py index 5b92c89d..84458c9e 100644 --- a/test_opensearchpy/test_async/test_signer.py +++ b/test_opensearchpy/test_async/test_signer.py @@ -8,7 +8,6 @@ # Modifications Copyright OpenSearch Contributors. See # GitHub history for details. -import sys import uuid import pytest @@ -42,9 +41,6 @@ async def test_aws_signer_async_as_http_auth(self): assert "X-Amz-Date" in headers assert "X-Amz-Security-Token" in headers - @pytest.mark.skipif( - sys.version_info < (3, 6), reason="AWSV4SignerAuth requires python3.6+" - ) async def test_aws_signer_async_when_region_is_null(self): session = self.mock_session() @@ -58,9 +54,6 @@ async def test_aws_signer_async_when_region_is_null(self): AWSV4SignerAsyncAuth(session, "") assert str(e.value) == "Region cannot be empty" - @pytest.mark.skipif( - sys.version_info < (3, 6), reason="AWSV4SignerAuth requires python3.6+" - ) async def test_aws_signer_async_when_credentials_is_null(self): region = "us-west-1" @@ -70,9 +63,6 @@ async def test_aws_signer_async_when_credentials_is_null(self): AWSV4SignerAsyncAuth(None, region) assert str(e.value) == "Credentials cannot be empty" - @pytest.mark.skipif( - sys.version_info < (3, 6), reason="AWSV4SignerAsyncAuth requires python3.6+" - ) async def test_aws_signer_async_when_service_is_specified(self): region = "us-west-2" service = "aoss" @@ -100,9 +90,6 @@ def mock_session(self, disable_get_frozen=True): return dummy_session - @pytest.mark.skipif( - sys.version_info < (3, 6), reason="AWSV4SignerAsyncAuth requires python3.6+" - ) async def test_aws_signer_async_frozen_credentials_as_http_auth(self): region = "us-west-2" diff --git a/test_opensearchpy/test_connection.py b/test_opensearchpy/test_connection.py deleted file mode 100644 index 5ec6e09d..00000000 --- a/test_opensearchpy/test_connection.py +++ /dev/null @@ -1,1171 +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 TestUrllib3Connection(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 ptyhon" - ) - - 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="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_region_is_null(self): - session = self.mock_session() - - from opensearchpy.helpers.signer import AWSV4SignerAuth - - with pytest.raises(ValueError) as e: - AWSV4SignerAuth(session, None) - assert str(e.value) == "Region cannot be empty" - - with pytest.raises(ValueError) as e: - AWSV4SignerAuth(session, "") - assert str(e.value) == "Region cannot be empty" - - @pytest.mark.skipif( - sys.version_info < (3, 6), reason="AWSV4SignerAuth requires python3.6+" - ) - def test_aws_signer_when_credentials_is_null(self): - region = "us-west-1" - - from opensearchpy.helpers.signer import AWSV4SignerAuth - - with pytest.raises(ValueError) as e: - AWSV4SignerAuth(None, region) - assert str(e.value) == "Credentials cannot be empty" - - with pytest.raises(ValueError) as e: - AWSV4SignerAuth("", region) - assert str(e.value) == "Credentials cannot be empty" - - @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) - - 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(TestUrllib3Connection): - 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="AWSV4SignerAuth requires python3.6+" - ) - def test_aws_signer_frozen_credentials_as_http_auth(self): - region = "us-west-2" - - import requests - - from opensearchpy.helpers.signer import AWSV4SignerAuth - - mock_session = self.mock_session() - - auth = AWSV4SignerAuth(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 TestRequestsConnection(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!" - - -@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..c85d2efd --- /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 uuid +import warnings + +import pytest +from mock import Mock, patch +from requests.auth import AuthBase + +from opensearchpy.connection import Connection, RequestsHttpConnection +from opensearchpy.exceptions import ( + ConflictError, + NotFoundError, + RequestError, + TransportError, +) +from test_opensearchpy.TestHttpServer import TestHTTPServer + +from ..test_cases import TestCase + + +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 + + 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 + + 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) + + 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) + + @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), + ) + + +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 + + 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..929258fd --- /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 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.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, + ) + + @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) + + 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) + + 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" + + 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" + + 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 + + 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 + + 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() diff --git a/test_opensearchpy/test_helpers/test_field.py b/test_opensearchpy/test_helpers/test_field.py index 15b51c52..288eab3a 100644 --- a/test_opensearchpy/test_helpers/test_field.py +++ b/test_opensearchpy/test_helpers/test_field.py @@ -25,7 +25,6 @@ # under the License. import base64 -import sys from datetime import datetime from ipaddress import ip_address @@ -150,7 +149,6 @@ def test_scaled_float(): assert f.to_dict() == {"scaling_factor": 123, "type": "scaled_float"} -@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6 or higher") def test_ipaddress(): f = field.Ip() assert f.deserialize("127.0.0.1") == ip_address("127.0.0.1") diff --git a/test_opensearchpy/test_server/test_rest_api_spec.py b/test_opensearchpy/test_server/test_rest_api_spec.py index b5d890ab..306993f2 100644 --- a/test_opensearchpy/test_server/test_rest_api_spec.py +++ b/test_opensearchpy/test_server/test_rest_api_spec.py @@ -33,7 +33,6 @@ import io import os import re -import sys import warnings import zipfile @@ -135,8 +134,7 @@ OPENSEARCH_VERSION = None RUN_ASYNC_REST_API_TESTS = ( - sys.version_info >= (3, 6) - and os.environ.get("PYTHON_CONNECTION_CLASS") == "RequestsHttpConnection" + os.environ.get("PYTHON_CONNECTION_CLASS") == "RequestsHttpConnection" ) FALSEY_VALUES = ("", None, False, 0, 0.0)