diff --git a/.buildkite/generatesteps.py b/.buildkite/generatesteps.py index b1de838..b054287 100644 --- a/.buildkite/generatesteps.py +++ b/.buildkite/generatesteps.py @@ -14,7 +14,7 @@ def benchmark_to_steps(python, connection_class): "env": { "PYTHON_VERSION": f"{python}", "PYTHON_CONNECTION_CLASS": f"{connection_class}", - # TEMPORARY for 3.11 + # For development versions # https://github.com/aio-libs/aiohttp/issues/6600 "AIOHTTP_NO_EXTENSIONS": 1, # https://github.com/aio-libs/frozenlist/issues/285 @@ -53,7 +53,7 @@ def benchmark_to_steps(python, connection_class): if __name__ == "__main__": steps = [] - for python in ["3.7", "3.8", "3.9", "3.10", "3.11"]: + for python in ["3.9", "3.10", "3.11", "3.12"]: for connection_class in ["urllib3", "requests"]: steps.extend(benchmark_to_steps(python, connection_class)) print(yaml.dump({"steps": steps}, Dumper=yaml.Dumper, sort_keys=False)) diff --git a/.buildkite/run-tests b/.buildkite/run-tests index 322bd39..c2a7497 100755 --- a/.buildkite/run-tests +++ b/.buildkite/run-tests @@ -3,7 +3,7 @@ set -euo pipefail # Default environment variables export FORCE_COLOR=1 -export PYTHON_VERSION="${PYTHON_VERSION:=3.9}" +export PYTHON_VERSION="${PYTHON_VERSION:=3.12}" export PYTHON_CONNECTION_CLASS="${PYTHON_CONNECTION_CLASS:=urllib3}" export EC_PROJECT_NAME="$EC_PROJECT_PREFIX-$BUILDKITE_JOB_ID" buildkite-agent meta-data set $EC_PROJECT_PREFIX $EC_PROJECT_NAME diff --git a/.buildkite/teardown-tests b/.buildkite/teardown-tests index fac9391..81b70e5 100644 --- a/.buildkite/teardown-tests +++ b/.buildkite/teardown-tests @@ -2,7 +2,7 @@ set -euo pipefail # Default environment variables -export PYTHON_VERSION="${PYTHON_VERSION:=3.9}" +export PYTHON_VERSION="${PYTHON_VERSION:=3.12}" export PYTHON_CONNECTION_CLASS="${PYTHON_CONNECTION_CLASS:=urllib3}" export EC_PROJECT_NAME=$(buildkite-agent meta-data get $EC_PROJECT_PREFIX) diff --git a/.github/Dockerfile b/.github/Dockerfile index 4fef09d..341f3ca 100644 --- a/.github/Dockerfile +++ b/.github/Dockerfile @@ -1,4 +1,4 @@ -ARG PYTHON_VERSION=3.9 +ARG PYTHON_VERSION=3.12 FROM python:${PYTHON_VERSION} WORKDIR /code/elasticsearch-serverless-python diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 66a1417..d0900d7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -59,7 +59,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.9", "3.10", "3.11", "3.12"] experimental: [false] nox-session: [""] runs-on: ["ubuntu-latest"] @@ -74,6 +74,7 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + allow-prereleases: true - name: Install dependencies run: | python -m pip install nox @@ -83,7 +84,7 @@ jobs: env: PYTHON_VERSION: ${{ matrix.python-version }} NOX_SESSION: ${{ matrix.nox-session }} - # TEMPORARY for 3.11 + # For development versions # https://github.com/aio-libs/aiohttp/issues/6600 AIOHTTP_NO_EXTENSIONS: 1 # https://github.com/aio-libs/frozenlist/issues/285 diff --git a/docs/guide/configuration.asciidoc b/docs/guide/configuration.asciidoc index e4d8584..61bc44e 100644 --- a/docs/guide/configuration.asciidoc +++ b/docs/guide/configuration.asciidoc @@ -52,7 +52,7 @@ es = Elasticsearch( [discrete] ==== TLS versions -Configuring the minimum TLS version to connect to is done via the `ssl_version` parameter. By default this is set to a minimum value of TLSv1.2. In Python 3.7+ you can use the new `ssl.TLSVersion` enumeration to specify versions. +Configuring the minimum TLS version to connect to is done via the `ssl_version` parameter. By default this is set to a minimum value of TLSv1.2. You can use the new `ssl.TLSVersion` enumeration to specify versions. [source,python] ------------------------------------ diff --git a/docs/guide/getting-started.asciidoc b/docs/guide/getting-started.asciidoc index 7a9149d..904bc3b 100644 --- a/docs/guide/getting-started.asciidoc +++ b/docs/guide/getting-started.asciidoc @@ -8,7 +8,7 @@ operations with it. [discrete] === Requirements -* https://www.python.org/[Python] 3.7 or newer +* https://www.python.org/[Python] 3.9 or newer * https://pip.pypa.io/en/stable/[`pip`], installed by default alongside Python [discrete] diff --git a/noxfile.py b/noxfile.py index 3332867..669db2f 100644 --- a/noxfile.py +++ b/noxfile.py @@ -31,7 +31,7 @@ INSTALL_ENV = {"AIOHTTP_NO_EXTENSIONS": "1"} -@nox.session(python=["3.7", "3.8", "3.9", "3.10", "3.11"]) +@nox.session(python=["3.9", "3.10", "3.11", "3.12"]) def test(session): session.install(".[dev]", env=INSTALL_ENV) diff --git a/pyproject.toml b/pyproject.toml index 618488a..b6c9001 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ version = "0.2.0.20231031" description = "Python client for Elasticsearch Serverless" readme = "README.rst" license = "Apache-2.0" -requires-python = ">=3.7, <4" +requires-python = ">=3.9" authors = [ { name = "Elastic Client Library Maintainers", email = "client-libs@elastic.co" }, ] @@ -22,13 +22,12 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", ] keywords = [ "elasticsearch", diff --git a/test_elasticsearch_serverless/test_async/test_server/test_helpers.py b/test_elasticsearch_serverless/test_async/test_server/test_helpers.py index d0aac44..601fe35 100644 --- a/test_elasticsearch_serverless/test_async/test_server/test_helpers.py +++ b/test_elasticsearch_serverless/test_async/test_server/test_helpers.py @@ -498,9 +498,10 @@ async def test_scroll_error(self, async_client, scan_teardown): bulk.append({"value": x}) await async_client.bulk(operations=bulk, refresh=True) - with patch.object( - async_client, "options", return_value=async_client - ), patch.object(async_client, "scroll", MockScroll()): + with ( + patch.object(async_client, "options", return_value=async_client), + patch.object(async_client, "scroll", MockScroll()), + ): data = [ x async for x in helpers.async_scan( @@ -514,9 +515,10 @@ async def test_scroll_error(self, async_client, scan_teardown): assert len(data) == 3 assert data[-1] == {"scroll_data": 42} - with patch.object( - async_client, "options", return_value=async_client - ), patch.object(async_client, "scroll", MockScroll()): + with ( + patch.object(async_client, "options", return_value=async_client), + patch.object(async_client, "scroll", MockScroll()), + ): with pytest.raises(ScanError): data = [ x @@ -532,9 +534,10 @@ async def test_scroll_error(self, async_client, scan_teardown): assert data[-1] == {"scroll_data": 42} async def test_initial_search_error(self, async_client, scan_teardown): - with patch.object( - async_client, "options", return_value=async_client - ), patch.object(async_client, "clear_scroll", new_callable=AsyncMock): + with ( + patch.object(async_client, "options", return_value=async_client), + patch.object(async_client, "clear_scroll", new_callable=AsyncMock), + ): with patch.object( async_client, "search", @@ -590,15 +593,16 @@ async def test_initial_search_error(self, async_client, scan_teardown): assert mock_scroll.calls == [] async def test_no_scroll_id_fast_route(self, async_client, scan_teardown): - with patch.object( - async_client, "options", return_value=async_client - ), patch.object(async_client, "scroll") as scroll_mock, patch.object( - async_client, - "search", - MockResponse(ObjectApiResponse(body={"no": "_scroll_id"}, meta=None)), - ), patch.object( - async_client, "clear_scroll" - ) as clear_mock: + with ( + patch.object(async_client, "options", return_value=async_client), + patch.object(async_client, "scroll") as scroll_mock, + patch.object( + async_client, + "search", + MockResponse(ObjectApiResponse(body={"no": "_scroll_id"}, meta=None)), + ), + patch.object(async_client, "clear_scroll") as clear_mock, + ): data = [ x async for x in helpers.async_scan(async_client, index="test_index") ] @@ -615,9 +619,10 @@ async def test_logger(self, logger_mock, async_client, scan_teardown): bulk.append({"value": x}) await async_client.bulk(operations=bulk, refresh=True) - with patch.object( - async_client, "options", return_value=async_client - ), patch.object(async_client, "scroll", MockScroll()): + with ( + patch.object(async_client, "options", return_value=async_client), + patch.object(async_client, "scroll", MockScroll()), + ): _ = [ x async for x in helpers.async_scan( @@ -630,9 +635,10 @@ async def test_logger(self, logger_mock, async_client, scan_teardown): ] logger_mock.warning.assert_called() - with patch.object( - async_client, "options", return_value=async_client - ), patch.object(async_client, "scroll", MockScroll()): + with ( + patch.object(async_client, "options", return_value=async_client), + patch.object(async_client, "scroll", MockScroll()), + ): try: _ = [ x @@ -660,11 +666,12 @@ async def test_clear_scroll(self, async_client, scan_teardown): bulk.append({"value": x}) await async_client.bulk(operations=bulk, refresh=True) - with patch.object( - async_client, "options", return_value=async_client - ), patch.object( - async_client, "clear_scroll", wraps=async_client.clear_scroll - ) as spy: + with ( + patch.object(async_client, "options", return_value=async_client), + patch.object( + async_client, "clear_scroll", wraps=async_client.clear_scroll + ) as spy, + ): _ = [ x async for x in helpers.async_scan( @@ -702,20 +709,21 @@ async def test_clear_scroll(self, async_client, scan_teardown): async def test_scan_auth_kwargs_forwarded( self, async_client, scan_teardown, kwargs ): - with patch.object( - async_client, "options", return_value=async_client - ) as options, patch.object( - async_client, - "search", - return_value=MockResponse( - ObjectApiResponse( - body={ - "_scroll_id": "scroll_id", - "_shards": {"successful": 5, "total": 5, "skipped": 0}, - "hits": {"hits": [{"search_data": 1}]}, - }, - meta=None, - ) + with ( + patch.object(async_client, "options", return_value=async_client) as options, + patch.object( + async_client, + "search", + return_value=MockResponse( + ObjectApiResponse( + body={ + "_scroll_id": "scroll_id", + "_shards": {"successful": 5, "total": 5, "skipped": 0}, + "hits": {"hits": [{"search_data": 1}]}, + }, + meta=None, + ) + ), ), ): with patch.object( @@ -755,20 +763,21 @@ async def test_scan_auth_kwargs_forwarded( async def test_scan_auth_kwargs_favor_scroll_kwargs_option( self, async_client, scan_teardown ): - with patch.object( - async_client, "options", return_value=async_client - ) as options, patch.object( - async_client, - "search", - return_value=MockResponse( - ObjectApiResponse( - body={ - "_scroll_id": "scroll_id", - "_shards": {"successful": 5, "total": 5, "skipped": 0}, - "hits": {"hits": [{"search_data": 1}]}, - }, - meta=None, - ) + with ( + patch.object(async_client, "options", return_value=async_client) as options, + patch.object( + async_client, + "search", + return_value=MockResponse( + ObjectApiResponse( + body={ + "_scroll_id": "scroll_id", + "_shards": {"successful": 5, "total": 5, "skipped": 0}, + "hits": {"hits": [{"search_data": 1}]}, + }, + meta=None, + ) + ), ), ): with patch.object( @@ -832,21 +841,23 @@ async def test_scan_auth_kwargs_favor_scroll_kwargs_option( ], ) async def test_scan_from_keyword_is_aliased(async_client, scan_kwargs): - with patch.object(async_client, "options", return_value=async_client), patch.object( - async_client, - "search", - return_value=MockResponse( - ObjectApiResponse( - body={ - "_scroll_id": "dummy_id", - "_shards": {"successful": 5, "total": 5}, - "hits": {"hits": []}, - }, - meta=None, - ) - ), - ) as search_mock, patch.object( - async_client, "clear_scroll", return_value=MockResponse(None) + with ( + patch.object(async_client, "options", return_value=async_client), + patch.object( + async_client, + "search", + return_value=MockResponse( + ObjectApiResponse( + body={ + "_scroll_id": "dummy_id", + "_shards": {"successful": 5, "total": 5}, + "hits": {"hits": []}, + }, + meta=None, + ) + ), + ) as search_mock, + patch.object(async_client, "clear_scroll", return_value=MockResponse(None)), ): [ x diff --git a/test_elasticsearch_serverless/test_server/test_helpers.py b/test_elasticsearch_serverless/test_server/test_helpers.py index f0e6fce..f804866 100644 --- a/test_elasticsearch_serverless/test_server/test_helpers.py +++ b/test_elasticsearch_serverless/test_server/test_helpers.py @@ -474,9 +474,10 @@ def test_scroll_error(sync_client): bulk.append({"value": x}) sync_client.bulk(operations=bulk, refresh=True) - with patch.object(sync_client, "options", return_value=sync_client), patch.object( - sync_client, "scroll" - ) as scroll_mock: + with ( + patch.object(sync_client, "options", return_value=sync_client), + patch.object(sync_client, "scroll") as scroll_mock, + ): scroll_mock.side_effect = mock_scroll_responses data = list( helpers.scan( @@ -506,21 +507,25 @@ def test_scroll_error(sync_client): def test_initial_search_error(sync_client): - with patch.object( - sync_client, - "search", - return_value=ObjectApiResponse( - meta=None, - raw={ - "_scroll_id": "dummy_id", - "_shards": {"successful": 4, "total": 5, "skipped": 0}, - "hits": {"hits": [{"search_data": 1}]}, - }, + with ( + patch.object( + sync_client, + "search", + return_value=ObjectApiResponse( + meta=None, + raw={ + "_scroll_id": "dummy_id", + "_shards": {"successful": 4, "total": 5, "skipped": 0}, + "hits": {"hits": [{"search_data": 1}]}, + }, + ), ), - ), patch.object(sync_client, "options", return_value=sync_client): - with patch.object(sync_client, "scroll") as scroll_mock, patch.object( - sync_client, "clear_scroll" - ) as clear_scroll_mock: + patch.object(sync_client, "options", return_value=sync_client), + ): + with ( + patch.object(sync_client, "scroll") as scroll_mock, + patch.object(sync_client, "clear_scroll") as clear_scroll_mock, + ): scroll_mock.side_effect = mock_scroll_responses data = list( helpers.scan( @@ -538,9 +543,10 @@ def test_initial_search_error(sync_client): scroll_id="dummy_id", ) - with patch.object(sync_client, "scroll") as scroll_mock, patch.object( - sync_client, "clear_scroll" - ) as clear_scroll_mock: + with ( + patch.object(sync_client, "scroll") as scroll_mock, + patch.object(sync_client, "clear_scroll") as clear_scroll_mock, + ): scroll_mock.side_effect = mock_scroll_responses with pytest.raises(ScanError): data = list( @@ -558,15 +564,16 @@ def test_initial_search_error(sync_client): def test_no_scroll_id_fast_route(sync_client): - with patch.object( - sync_client, - "search", - return_value=ObjectApiResponse(meta=None, raw={"no": "_scroll_id"}), - ) as search_mock, patch.object(sync_client, "scroll") as scroll_mock, patch.object( - sync_client, "clear_scroll" - ) as clear_scroll_mock, patch.object( - sync_client, "options", return_value=sync_client - ) as options: + with ( + patch.object( + sync_client, + "search", + return_value=ObjectApiResponse(meta=None, raw={"no": "_scroll_id"}), + ) as search_mock, + patch.object(sync_client, "scroll") as scroll_mock, + patch.object(sync_client, "clear_scroll") as clear_scroll_mock, + patch.object(sync_client, "options", return_value=sync_client) as options, + ): data = list(helpers.scan(sync_client, index="test_index")) assert data == [] @@ -595,32 +602,37 @@ def test_no_scroll_id_fast_route(sync_client): def test_scan_auth_kwargs_forwarded(sync_client, kwargs): ((key, val),) = kwargs.items() - with patch.object( - sync_client, "options", return_value=sync_client - ) as options, patch.object( - sync_client, - "search", - return_value=ObjectApiResponse( - meta=None, - raw={ - "_scroll_id": "scroll_id", - "_shards": {"successful": 5, "total": 5, "skipped": 0}, - "hits": {"hits": [{"search_data": 1}]}, - }, + with ( + patch.object(sync_client, "options", return_value=sync_client) as options, + patch.object( + sync_client, + "search", + return_value=ObjectApiResponse( + meta=None, + raw={ + "_scroll_id": "scroll_id", + "_shards": {"successful": 5, "total": 5, "skipped": 0}, + "hits": {"hits": [{"search_data": 1}]}, + }, + ), ), - ), patch.object( - sync_client, - "scroll", - return_value=ObjectApiResponse( - meta=None, - raw={ - "_scroll_id": "scroll_id", - "_shards": {"successful": 5, "total": 5, "skipped": 0}, - "hits": {"hits": []}, - }, + patch.object( + sync_client, + "scroll", + return_value=ObjectApiResponse( + meta=None, + raw={ + "_scroll_id": "scroll_id", + "_shards": {"successful": 5, "total": 5, "skipped": 0}, + "hits": {"hits": []}, + }, + ), + ), + patch.object( + sync_client, + "clear_scroll", + return_value=ObjectApiResponse(meta=None, raw={}), ), - ), patch.object( - sync_client, "clear_scroll", return_value=ObjectApiResponse(meta=None, raw={}) ): data = list(helpers.scan(sync_client, index="test_index", **kwargs)) @@ -635,32 +647,37 @@ def test_scan_auth_kwargs_forwarded(sync_client, kwargs): def test_scan_auth_kwargs_favor_scroll_kwargs_option(sync_client): - with patch.object( - sync_client, "options", return_value=sync_client - ) as options_mock, patch.object( - sync_client, - "search", - return_value=ObjectApiResponse( - raw={ - "_scroll_id": "scroll_id", - "_shards": {"successful": 5, "total": 5, "skipped": 0}, - "hits": {"hits": [{"search_data": 1}]}, - }, - meta=None, - ), - ) as search_mock, patch.object( - sync_client, - "scroll", - return_value=ObjectApiResponse( - raw={ - "_scroll_id": "scroll_id", - "_shards": {"successful": 5, "total": 5, "skipped": 0}, - "hits": {"hits": []}, - }, - meta=None, + with ( + patch.object(sync_client, "options", return_value=sync_client) as options_mock, + patch.object( + sync_client, + "search", + return_value=ObjectApiResponse( + raw={ + "_scroll_id": "scroll_id", + "_shards": {"successful": 5, "total": 5, "skipped": 0}, + "hits": {"hits": [{"search_data": 1}]}, + }, + meta=None, + ), + ) as search_mock, + patch.object( + sync_client, + "scroll", + return_value=ObjectApiResponse( + raw={ + "_scroll_id": "scroll_id", + "_shards": {"successful": 5, "total": 5, "skipped": 0}, + "hits": {"hits": []}, + }, + meta=None, + ), + ) as scroll_mock, + patch.object( + sync_client, + "clear_scroll", + return_value=ObjectApiResponse(raw={}, meta=None), ), - ) as scroll_mock, patch.object( - sync_client, "clear_scroll", return_value=ObjectApiResponse(raw={}, meta=None) ): data = list( helpers.scan( @@ -694,13 +711,11 @@ def test_log_warning_on_shard_failures(sync_client): bulk.append({"value": x}) sync_client.bulk(operations=bulk, refresh=True) - with patch( - "elasticsearch_serverless.helpers.actions.logger" - ) as logger_mock, patch.object( - sync_client, "options", return_value=sync_client - ), patch.object( - sync_client, "scroll" - ) as scroll_mock: + with ( + patch("elasticsearch_serverless.helpers.actions.logger") as logger_mock, + patch.object(sync_client, "options", return_value=sync_client), + patch.object(sync_client, "scroll") as scroll_mock, + ): scroll_mock.side_effect = mock_scroll_responses list( helpers.scan( @@ -736,9 +751,12 @@ def test_clear_scroll(sync_client): bulk.append({"value": x}) sync_client.bulk(operations=bulk, refresh=True) - with patch.object(sync_client, "options", return_value=sync_client), patch.object( - sync_client, "clear_scroll", wraps=sync_client.clear_scroll - ) as clear_scroll_mock: + with ( + patch.object(sync_client, "options", return_value=sync_client), + patch.object( + sync_client, "clear_scroll", wraps=sync_client.clear_scroll + ) as clear_scroll_mock, + ): list(helpers.scan(sync_client, index="test_index", size=2)) clear_scroll_mock.assert_called_once() @@ -753,19 +771,22 @@ def test_clear_scroll(sync_client): def test_shards_no_skipped_field(sync_client): # Test that scan doesn't fail if 'hits.skipped' isn't available. - with patch.object(sync_client, "options", return_value=sync_client), patch.object( - sync_client, - "search", - return_value=ObjectApiResponse( - raw={ - "_scroll_id": "dummy_id", - "_shards": {"successful": 5, "total": 5}, - "hits": {"hits": [{"search_data": 1}]}, - }, - meta=None, + with ( + patch.object(sync_client, "options", return_value=sync_client), + patch.object( + sync_client, + "search", + return_value=ObjectApiResponse( + raw={ + "_scroll_id": "dummy_id", + "_shards": {"successful": 5, "total": 5}, + "hits": {"hits": [{"search_data": 1}]}, + }, + meta=None, + ), ), - ), patch.object(sync_client, "scroll") as scroll_mock, patch.object( - sync_client, "clear_scroll" + patch.object(sync_client, "scroll") as scroll_mock, + patch.object(sync_client, "clear_scroll"), ): scroll_mock.side_effect = [ ObjectApiResponse( @@ -804,18 +825,22 @@ def test_shards_no_skipped_field(sync_client): ], ) def test_scan_from_keyword_is_aliased(sync_client, scan_kwargs): - with patch.object(sync_client, "options", return_value=sync_client), patch.object( - sync_client, - "search", - return_value=ObjectApiResponse( - raw={ - "_scroll_id": "dummy_id", - "_shards": {"successful": 5, "total": 5}, - "hits": {"hits": []}, - }, - meta=None, - ), - ) as search_mock, patch.object(sync_client, "clear_scroll"): + with ( + patch.object(sync_client, "options", return_value=sync_client), + patch.object( + sync_client, + "search", + return_value=ObjectApiResponse( + raw={ + "_scroll_id": "dummy_id", + "_shards": {"successful": 5, "total": 5}, + "hits": {"hits": []}, + }, + meta=None, + ), + ) as search_mock, + patch.object(sync_client, "clear_scroll"), + ): list(helpers.scan(sync_client, index="test_index", **scan_kwargs)) assert search_mock.call_args[1]["from_"] == 1 assert "from" not in search_mock.call_args[1]