Rename module to opensearchpy
To avoid conflict with an existing package by name 'opensearch' being present Signed-off-by: Rushi Agrawal <[email protected]>
This commit is contained in:
@@ -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.
|
||||
Executable
+162
@@ -0,0 +1,162 @@
|
||||
#!/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.
|
||||
#
|
||||
# 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.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from os import environ
|
||||
from os.path import abspath, dirname, exists, join, pardir
|
||||
|
||||
|
||||
def fetch_opensearch_repo():
|
||||
# user is manually setting YAML dir, don't tamper with it
|
||||
if "TEST_OPENSEARCH_YAML_DIR" in environ:
|
||||
return
|
||||
|
||||
repo_path = environ.get(
|
||||
"TEST_OPENSEARCH_REPO",
|
||||
abspath(join(dirname(__file__), pardir, pardir, "opensearch")),
|
||||
)
|
||||
|
||||
# no repo
|
||||
if not exists(repo_path) or not exists(join(repo_path, ".git")):
|
||||
subprocess.check_call(
|
||||
"git clone https://github.com/opensearch-project/opensearch %s" % repo_path,
|
||||
shell=True,
|
||||
)
|
||||
|
||||
# set YAML test dir
|
||||
environ["TEST_OPENSEARCH_YAML_DIR"] = join(
|
||||
repo_path, "rest-api-spec", "src", "main", "resources", "rest-api-spec", "test"
|
||||
)
|
||||
|
||||
# fetching of yaml tests disabled, we'll run with what's there
|
||||
if environ.get("TEST_OPENSEARCH_NOFETCH", False):
|
||||
return
|
||||
|
||||
from test_opensearchpy.test_cases import SkipTest
|
||||
from test_opensearchpy.test_server import get_client
|
||||
|
||||
# find out the sha of the running client
|
||||
try:
|
||||
client = get_client()
|
||||
sha = client.info()["version"]["build_hash"]
|
||||
except (SkipTest, KeyError):
|
||||
print("No running opensearch >1.X server...")
|
||||
return
|
||||
|
||||
# fetch new commits to be sure...
|
||||
print("Fetching opensearch repo...")
|
||||
subprocess.check_call(
|
||||
"cd %s && git fetch https://github.com/opensearch-project/opensearch.git"
|
||||
% repo_path,
|
||||
shell=True,
|
||||
)
|
||||
# reset to the version from info()
|
||||
subprocess.check_call("cd %s && git fetch" % repo_path, shell=True)
|
||||
subprocess.check_call("cd %s && git reset --hard %s" % (repo_path, sha), shell=True)
|
||||
|
||||
|
||||
def run_all(argv=None):
|
||||
sys.exitfunc = lambda: sys.stderr.write("Shutting down....\n")
|
||||
|
||||
# fetch yaml tests anywhere that's not GitHub Actions
|
||||
if "GITHUB_ACTION" not in environ:
|
||||
fetch_opensearch_repo()
|
||||
|
||||
# always insert coverage when running tests
|
||||
if argv is None:
|
||||
junit_xml = join(
|
||||
abspath(dirname(dirname(__file__))), "junit", "opensearch-py-junit.xml"
|
||||
)
|
||||
argv = [
|
||||
"pytest",
|
||||
"--cov=opensearch",
|
||||
"--junitxml=%s" % junit_xml,
|
||||
"--log-level=DEBUG",
|
||||
"--cache-clear",
|
||||
"-vv",
|
||||
]
|
||||
|
||||
secured = False
|
||||
if environ.get("OPENSEARCH_URL", "").startswith("https://"):
|
||||
secured = True
|
||||
|
||||
ignores = []
|
||||
# Python 3.6+ is required for async
|
||||
if sys.version_info < (3, 6):
|
||||
ignores.append("test_opensearchpy/test_async/")
|
||||
|
||||
# GitHub Actions, run non-server tests
|
||||
if "GITHUB_ACTION" in environ:
|
||||
ignores.extend(
|
||||
[
|
||||
"test_opensearchpy/test_server/",
|
||||
"test_opensearchpy/test_server_secured/",
|
||||
"test_opensearchpy/test_async/test_server/",
|
||||
]
|
||||
)
|
||||
|
||||
# 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"))
|
||||
ignores.extend(
|
||||
[
|
||||
"test_opensearchpy/test_server/",
|
||||
"test_opensearchpy/test_async/test_server/",
|
||||
]
|
||||
)
|
||||
else:
|
||||
argv.append(join(test_dir, "test_server"))
|
||||
if sys.version_info >= (3, 6):
|
||||
argv.append(join(test_dir, "test_async/test_server"))
|
||||
ignores.extend(
|
||||
[
|
||||
"test_opensearchpy/test_server_secured/",
|
||||
]
|
||||
)
|
||||
|
||||
if ignores:
|
||||
argv.extend(["--ignore=%s" % ignore for ignore in ignores])
|
||||
|
||||
# Not in CI, run all tests specified.
|
||||
else:
|
||||
argv.append(abspath(dirname(__file__)))
|
||||
|
||||
exit_code = 0
|
||||
try:
|
||||
subprocess.check_call(argv, stdout=sys.stdout, stderr=sys.stderr)
|
||||
except subprocess.CalledProcessError as e:
|
||||
exit_code = e.returncode
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_all(sys.argv)
|
||||
@@ -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.
|
||||
@@ -0,0 +1,421 @@
|
||||
# -*- 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 ssl
|
||||
import warnings
|
||||
from platform import python_version
|
||||
|
||||
import aiohttp
|
||||
import pytest
|
||||
from mock import patch
|
||||
from multidict import CIMultiDict
|
||||
|
||||
from opensearchpy import AIOHttpConnection, __versionstr__
|
||||
from opensearchpy.compat import reraise_exceptions
|
||||
from opensearchpy.exceptions import ConnectionError
|
||||
|
||||
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, connection_params={}, response_body=b"{}"):
|
||||
con = AIOHttpConnection(**connection_params)
|
||||
await con._create_aiohttp_session()
|
||||
|
||||
def _dummy_request(*args, **kwargs):
|
||||
class DummyResponse:
|
||||
async def __aenter__(self, *_, **__):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_, **__):
|
||||
pass
|
||||
|
||||
async def text(self):
|
||||
return response_body.decode("utf-8", "surrogatepass")
|
||||
|
||||
dummy_response = DummyResponse()
|
||||
dummy_response.headers = CIMultiDict()
|
||||
dummy_response.status = 200
|
||||
_dummy_request.call_args = (args, kwargs)
|
||||
return dummy_response
|
||||
|
||||
con.session.request = _dummy_request
|
||||
return con
|
||||
|
||||
async 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.
|
||||
pytest.skip(
|
||||
"Test test_ssl_context is skipped cause SSLContext is not available for this version of Python"
|
||||
)
|
||||
|
||||
con = AIOHttpConnection(use_ssl=True, ssl_context=context)
|
||||
await con._create_aiohttp_session()
|
||||
assert con.use_ssl
|
||||
assert con.session.connector._ssl == context
|
||||
|
||||
def test_opaque_id(self):
|
||||
con = AIOHttpConnection(opaque_id="app-1")
|
||||
assert con.headers["x-opaque-id"] == "app-1"
|
||||
|
||||
def test_http_cloud_id(self):
|
||||
con = AIOHttpConnection(
|
||||
cloud_id="cluster:dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyQ0ZmE4ODIxZTc1NjM0MDMyYmVkMWNmMjIxMTBlMmY5NyQ0ZmE4ODIxZTc1NjM0MDMyYmVkMWNmMjIxMTBlMmY5Ng=="
|
||||
)
|
||||
assert con.use_ssl
|
||||
assert (
|
||||
con.host
|
||||
== "https://4fa8821e75634032bed1cf22110e2f97.us-east-1.aws.found.io"
|
||||
)
|
||||
assert con.port is None
|
||||
assert con.hostname == "4fa8821e75634032bed1cf22110e2f97.us-east-1.aws.found.io"
|
||||
assert con.http_compress
|
||||
|
||||
con = AIOHttpConnection(
|
||||
cloud_id="cluster:dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyQ0ZmE4ODIxZTc1NjM0MDMyYmVkMWNmMjIxMTBlMmY5NyQ0ZmE4ODIxZTc1NjM0MDMyYmVkMWNmMjIxMTBlMmY5Ng==",
|
||||
port=9243,
|
||||
)
|
||||
assert (
|
||||
con.host
|
||||
== "https://4fa8821e75634032bed1cf22110e2f97.us-east-1.aws.found.io:9243"
|
||||
)
|
||||
assert con.port == 9243
|
||||
assert con.hostname == "4fa8821e75634032bed1cf22110e2f97.us-east-1.aws.found.io"
|
||||
|
||||
def test_api_key_auth(self):
|
||||
# test with tuple
|
||||
con = AIOHttpConnection(
|
||||
cloud_id="cluster:dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyQ0ZmE4ODIxZTc1NjM0MDMyYmVkMWNmMjIxMTBlMmY5NyQ0ZmE4ODIxZTc1NjM0MDMyYmVkMWNmMjIxMTBlMmY5Ng==",
|
||||
api_key=("elastic", "changeme1"),
|
||||
)
|
||||
assert con.headers["authorization"] == "ApiKey ZWxhc3RpYzpjaGFuZ2VtZTE="
|
||||
assert (
|
||||
con.host
|
||||
== "https://4fa8821e75634032bed1cf22110e2f97.us-east-1.aws.found.io"
|
||||
)
|
||||
|
||||
# test with base64 encoded string
|
||||
con = AIOHttpConnection(
|
||||
cloud_id="cluster:dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyQ0ZmE4ODIxZTc1NjM0MDMyYmVkMWNmMjIxMTBlMmY5NyQ0ZmE4ODIxZTc1NjM0MDMyYmVkMWNmMjIxMTBlMmY5Ng==",
|
||||
api_key="ZWxhc3RpYzpjaGFuZ2VtZTI=",
|
||||
)
|
||||
assert con.headers["authorization"] == "ApiKey ZWxhc3RpYzpjaGFuZ2VtZTI="
|
||||
assert (
|
||||
con.host
|
||||
== "https://4fa8821e75634032bed1cf22110e2f97.us-east-1.aws.found.io"
|
||||
)
|
||||
|
||||
async def test_no_http_compression(self):
|
||||
con = await self._get_mock_connection()
|
||||
assert not con.http_compress
|
||||
assert "accept-encoding" not in con.headers
|
||||
|
||||
await con.perform_request("GET", "/")
|
||||
|
||||
_, kwargs = con.session.request.call_args
|
||||
|
||||
assert not kwargs["data"]
|
||||
assert "accept-encoding" not in kwargs["headers"]
|
||||
assert "content-encoding" not in kwargs["headers"]
|
||||
|
||||
async def test_http_compression(self):
|
||||
con = await self._get_mock_connection({"http_compress": True})
|
||||
assert con.http_compress
|
||||
assert 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.
|
||||
assert "content-encoding" not in con.headers
|
||||
|
||||
await con.perform_request("GET", "/", body=b"{}")
|
||||
|
||||
_, kwargs = con.session.request.call_args
|
||||
|
||||
assert gzip_decompress(kwargs["data"]) == b"{}"
|
||||
assert kwargs["headers"]["accept-encoding"] == "gzip,deflate"
|
||||
assert kwargs["headers"]["content-encoding"] == "gzip"
|
||||
|
||||
await con.perform_request("GET", "/")
|
||||
|
||||
_, kwargs = con.session.request.call_args
|
||||
|
||||
assert not kwargs["data"]
|
||||
assert kwargs["headers"]["accept-encoding"] == "gzip,deflate"
|
||||
assert "content-encoding" not in kwargs["headers"]
|
||||
|
||||
def test_cloud_id_http_compress_override(self):
|
||||
# 'http_compress' will be 'True' by default for connections with
|
||||
# 'cloud_id' set but should prioritize user-defined values.
|
||||
con = AIOHttpConnection(
|
||||
cloud_id="cluster:dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyQ0ZmE4ODIxZTc1NjM0MDMyYmVkMWNmMjIxMTBlMmY5NyQ0ZmE4ODIxZTc1NjM0MDMyYmVkMWNmMjIxMTBlMmY5Ng==",
|
||||
)
|
||||
assert con.http_compress is True
|
||||
|
||||
con = AIOHttpConnection(
|
||||
cloud_id="cluster:dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyQ0ZmE4ODIxZTc1NjM0MDMyYmVkMWNmMjIxMTBlMmY5NyQ0ZmE4ODIxZTc1NjM0MDMyYmVkMWNmMjIxMTBlMmY5Ng==",
|
||||
http_compress=False,
|
||||
)
|
||||
assert con.http_compress is False
|
||||
|
||||
con = AIOHttpConnection(
|
||||
cloud_id="cluster:dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyQ0ZmE4ODIxZTc1NjM0MDMyYmVkMWNmMjIxMTBlMmY5NyQ0ZmE4ODIxZTc1NjM0MDMyYmVkMWNmMjIxMTBlMmY5Ng==",
|
||||
http_compress=True,
|
||||
)
|
||||
assert con.http_compress is True
|
||||
|
||||
async def test_url_prefix(self):
|
||||
con = await self._get_mock_connection(
|
||||
connection_params={"url_prefix": "/_search/"}
|
||||
)
|
||||
assert con.url_prefix == "/_search"
|
||||
|
||||
await con.perform_request("GET", "/")
|
||||
|
||||
# Need to convert the yarl URL to a string to compare.
|
||||
method, yarl_url = con.session.request.call_args[0]
|
||||
assert method == "GET" and str(yarl_url) == "http://localhost:9200/_search/"
|
||||
|
||||
def test_default_user_agent(self):
|
||||
con = AIOHttpConnection()
|
||||
assert con._get_default_user_agent() == "opensearch-py/%s (Python %s)" % (
|
||||
__versionstr__,
|
||||
python_version(),
|
||||
)
|
||||
|
||||
def test_timeout_set(self):
|
||||
con = AIOHttpConnection(timeout=42)
|
||||
assert 42 == con.timeout
|
||||
|
||||
def test_keep_alive_is_on_by_default(self):
|
||||
con = AIOHttpConnection()
|
||||
assert {
|
||||
"connection": "keep-alive",
|
||||
"content-type": "application/json",
|
||||
"user-agent": con._get_default_user_agent(),
|
||||
} == con.headers
|
||||
|
||||
def test_http_auth(self):
|
||||
con = AIOHttpConnection(http_auth="username:secret")
|
||||
assert {
|
||||
"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 = AIOHttpConnection(http_auth=("username", "secret"))
|
||||
assert {
|
||||
"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 = AIOHttpConnection(http_auth=["username", "secret"])
|
||||
assert {
|
||||
"authorization": "Basic dXNlcm5hbWU6c2VjcmV0",
|
||||
"content-type": "application/json",
|
||||
"connection": "keep-alive",
|
||||
"user-agent": con._get_default_user_agent(),
|
||||
} == con.headers
|
||||
|
||||
def test_uses_https_if_verify_certs_is_off(self):
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
con = AIOHttpConnection(use_ssl=True, verify_certs=False)
|
||||
assert 1 == len(w)
|
||||
assert (
|
||||
"Connecting to https://localhost:9200 using SSL with verify_certs=False is insecure."
|
||||
== str(w[0].message)
|
||||
)
|
||||
|
||||
assert con.use_ssl
|
||||
assert con.scheme == "https"
|
||||
assert con.host == "https://localhost:9200"
|
||||
|
||||
async def test_nowarn_when_test_uses_https_if_verify_certs_is_off(self):
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
con = AIOHttpConnection(
|
||||
use_ssl=True, verify_certs=False, ssl_show_warn=False
|
||||
)
|
||||
await con._create_aiohttp_session()
|
||||
assert w == []
|
||||
|
||||
assert isinstance(con.session, aiohttp.ClientSession)
|
||||
|
||||
def test_doesnt_use_https_if_not_specified(self):
|
||||
con = AIOHttpConnection()
|
||||
assert not con.use_ssl
|
||||
|
||||
def test_no_warning_when_using_ssl_context(self):
|
||||
ctx = ssl.create_default_context()
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
AIOHttpConnection(ssl_context=ctx)
|
||||
assert w == [], str([x.message for x in 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")
|
||||
|
||||
AIOHttpConnection(**kwargs)
|
||||
|
||||
assert 1 == len(w)
|
||||
assert (
|
||||
"When using `ssl_context`, all other SSL related kwargs are ignored"
|
||||
== str(w[0].message)
|
||||
)
|
||||
|
||||
@patch("opensearchpy.connection.base.logger")
|
||||
async def test_uncompressed_body_logged(self, logger):
|
||||
con = await self._get_mock_connection(connection_params={"http_compress": True})
|
||||
await con.perform_request("GET", "/", body=b'{"example": "body"}')
|
||||
|
||||
assert 2 == logger.debug.call_count
|
||||
req, resp = logger.debug.call_args_list
|
||||
|
||||
assert '> {"example": "body"}' == req[0][0] % req[0][1:]
|
||||
assert "< {}" == resp[0][0] % resp[0][1:]
|
||||
|
||||
async def test_surrogatepass_into_bytes(self):
|
||||
buf = b"\xe4\xbd\xa0\xe5\xa5\xbd\xed\xa9\xaa"
|
||||
con = await self._get_mock_connection(response_body=buf)
|
||||
status, headers, data = await con.perform_request("GET", "/")
|
||||
assert u"你好\uda6a" == data
|
||||
|
||||
@pytest.mark.parametrize("exception_cls", reraise_exceptions)
|
||||
async def test_recursion_error_reraised(self, exception_cls):
|
||||
conn = AIOHttpConnection()
|
||||
|
||||
def request_raise(*_, **__):
|
||||
raise exception_cls("Wasn't modified!")
|
||||
|
||||
await conn._create_aiohttp_session()
|
||||
conn.session.request = request_raise
|
||||
|
||||
with pytest.raises(exception_cls) as e:
|
||||
await conn.perform_request("GET", "/")
|
||||
assert str(e.value) == "Wasn't modified!"
|
||||
|
||||
|
||||
class TestConnectionHttpbin:
|
||||
"""Tests the HTTP connection implementations against a live server E2E"""
|
||||
|
||||
async def httpbin_anything(self, conn, **kwargs):
|
||||
status, headers, data = await conn.perform_request("GET", "/anything", **kwargs)
|
||||
data = json.loads(data)
|
||||
data["headers"].pop(
|
||||
"X-Amzn-Trace-Id", None
|
||||
) # Remove this header as it's put there by AWS.
|
||||
return (status, data)
|
||||
|
||||
async def test_aiohttp_connection(self):
|
||||
# Defaults
|
||||
conn = AIOHttpConnection("httpbin.org", port=443, use_ssl=True)
|
||||
user_agent = conn._get_default_user_agent()
|
||||
status, data = await self.httpbin_anything(conn)
|
||||
assert status == 200
|
||||
assert data["method"] == "GET"
|
||||
assert data["headers"] == {
|
||||
"Content-Type": "application/json",
|
||||
"Host": "httpbin.org",
|
||||
"User-Agent": user_agent,
|
||||
}
|
||||
|
||||
# http_compress=False
|
||||
conn = AIOHttpConnection(
|
||||
"httpbin.org", port=443, use_ssl=True, http_compress=False
|
||||
)
|
||||
status, data = await self.httpbin_anything(conn)
|
||||
assert status == 200
|
||||
assert data["method"] == "GET"
|
||||
assert data["headers"] == {
|
||||
"Content-Type": "application/json",
|
||||
"Host": "httpbin.org",
|
||||
"User-Agent": user_agent,
|
||||
}
|
||||
|
||||
# http_compress=True
|
||||
conn = AIOHttpConnection(
|
||||
"httpbin.org", port=443, use_ssl=True, http_compress=True
|
||||
)
|
||||
status, data = await self.httpbin_anything(conn)
|
||||
assert status == 200
|
||||
assert data["headers"] == {
|
||||
"Accept-Encoding": "gzip,deflate",
|
||||
"Content-Type": "application/json",
|
||||
"Host": "httpbin.org",
|
||||
"User-Agent": user_agent,
|
||||
}
|
||||
|
||||
# Headers
|
||||
conn = AIOHttpConnection(
|
||||
"httpbin.org",
|
||||
port=443,
|
||||
use_ssl=True,
|
||||
http_compress=True,
|
||||
headers={"header1": "value1"},
|
||||
)
|
||||
status, data = await self.httpbin_anything(
|
||||
conn, headers={"header2": "value2", "header1": "override!"}
|
||||
)
|
||||
assert status == 200
|
||||
assert data["headers"] == {
|
||||
"Accept-Encoding": "gzip,deflate",
|
||||
"Content-Type": "application/json",
|
||||
"Host": "httpbin.org",
|
||||
"Header1": "override!",
|
||||
"Header2": "value2",
|
||||
"User-Agent": user_agent,
|
||||
}
|
||||
|
||||
async def test_aiohttp_connection_error(self):
|
||||
conn = AIOHttpConnection("not.a.host.name")
|
||||
with pytest.raises(ConnectionError):
|
||||
await conn.perform_request("GET", "/")
|
||||
@@ -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.
|
||||
@@ -0,0 +1,65 @@
|
||||
# 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 asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
import opensearchpy
|
||||
from opensearchpy.helpers.test import CA_CERTS, OPENSEARCH_URL
|
||||
|
||||
from ...utils import wipe_cluster
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
async def async_client():
|
||||
client = None
|
||||
try:
|
||||
if not hasattr(opensearchpy, "AsyncOpenSearch"):
|
||||
pytest.skip("test requires 'AsyncOpenSearch'")
|
||||
|
||||
kw = {"timeout": 3, "ca_certs": CA_CERTS}
|
||||
client = opensearchpy.AsyncOpenSearch(OPENSEARCH_URL, **kw)
|
||||
|
||||
# wait for yellow status
|
||||
for _ in range(100):
|
||||
try:
|
||||
await client.cluster.health(wait_for_status="yellow")
|
||||
break
|
||||
except ConnectionError:
|
||||
await asyncio.sleep(0.1)
|
||||
else:
|
||||
# timeout
|
||||
pytest.skip("OpenSearch failed to start.")
|
||||
|
||||
yield client
|
||||
|
||||
finally:
|
||||
if client:
|
||||
wipe_cluster(client)
|
||||
await client.close()
|
||||
@@ -0,0 +1,66 @@
|
||||
# -*- 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.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
class TestUnicode:
|
||||
async def test_indices_analyze(self, async_client):
|
||||
await async_client.indices.analyze(body='{"text": "привет"}')
|
||||
|
||||
|
||||
class TestBulk:
|
||||
async def test_bulk_works_with_string_body(self, async_client):
|
||||
docs = '{ "index" : { "_index" : "bulk_test_index", "_id" : "1" } }\n{"answer": 42}'
|
||||
response = await async_client.bulk(body=docs)
|
||||
|
||||
assert response["errors"] is False
|
||||
assert len(response["items"]) == 1
|
||||
|
||||
async def test_bulk_works_with_bytestring_body(self, async_client):
|
||||
docs = b'{ "index" : { "_index" : "bulk_test_index", "_id" : "2" } }\n{"answer": 42}'
|
||||
response = await async_client.bulk(body=docs)
|
||||
|
||||
assert response["errors"] is False
|
||||
assert len(response["items"]) == 1
|
||||
|
||||
|
||||
class TestYarlMissing:
|
||||
async def test_aiohttp_connection_works_without_yarl(
|
||||
self, async_client, monkeypatch
|
||||
):
|
||||
# This is a defensive test case for if aiohttp suddenly stops using yarl.
|
||||
from opensearchpy._async import http_aiohttp
|
||||
|
||||
monkeypatch.setattr(http_aiohttp, "yarl", False)
|
||||
|
||||
resp = await async_client.info(pretty=True)
|
||||
assert isinstance(resp, dict)
|
||||
@@ -0,0 +1,901 @@
|
||||
# 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.
|
||||
|
||||
# Licensed to Elasticsearch B.V.under one or more agreements.
|
||||
# Elasticsearch B.V.licenses this file to you under the Apache 2.0 License.
|
||||
# See the LICENSE file in the project root for more information
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
from mock import MagicMock, patch
|
||||
|
||||
from opensearchpy import TransportError, helpers
|
||||
from opensearchpy.helpers import ScanError
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
class AsyncMock(MagicMock):
|
||||
async def __call__(self, *args, **kwargs):
|
||||
return super(AsyncMock, self).__call__(*args, **kwargs)
|
||||
|
||||
def __await__(self):
|
||||
return self().__await__()
|
||||
|
||||
|
||||
class FailingBulkClient(object):
|
||||
def __init__(
|
||||
self, client, fail_at=(2,), fail_with=TransportError(599, "Error!", {})
|
||||
):
|
||||
self.client = client
|
||||
self._called = 0
|
||||
self._fail_at = fail_at
|
||||
self.transport = client.transport
|
||||
self._fail_with = fail_with
|
||||
|
||||
async def bulk(self, *args, **kwargs):
|
||||
self._called += 1
|
||||
if self._called in self._fail_at:
|
||||
raise self._fail_with
|
||||
return await self.client.bulk(*args, **kwargs)
|
||||
|
||||
|
||||
class TestStreamingBulk(object):
|
||||
async def test_actions_remain_unchanged(self, async_client):
|
||||
actions = [{"_id": 1}, {"_id": 2}]
|
||||
async for ok, item in helpers.async_streaming_bulk(
|
||||
async_client, actions, index="test-index"
|
||||
):
|
||||
assert ok
|
||||
assert [{"_id": 1}, {"_id": 2}] == actions
|
||||
|
||||
async def test_all_documents_get_inserted(self, async_client):
|
||||
docs = [{"answer": x, "_id": x} for x in range(100)]
|
||||
async for ok, item in helpers.async_streaming_bulk(
|
||||
async_client, docs, index="test-index", refresh=True
|
||||
):
|
||||
assert ok
|
||||
|
||||
assert 100 == (await async_client.count(index="test-index"))["count"]
|
||||
assert {"answer": 42} == (await async_client.get(index="test-index", id=42))[
|
||||
"_source"
|
||||
]
|
||||
|
||||
async def test_documents_data_types(self, async_client):
|
||||
async def async_gen():
|
||||
for x in range(100):
|
||||
await asyncio.sleep(0)
|
||||
yield {"answer": x, "_id": x}
|
||||
|
||||
def sync_gen():
|
||||
for x in range(100):
|
||||
yield {"answer": x, "_id": x}
|
||||
|
||||
async for ok, item in helpers.async_streaming_bulk(
|
||||
async_client, async_gen(), index="test-index", refresh=True
|
||||
):
|
||||
assert ok
|
||||
|
||||
assert 100 == (await async_client.count(index="test-index"))["count"]
|
||||
assert {"answer": 42} == (await async_client.get(index="test-index", id=42))[
|
||||
"_source"
|
||||
]
|
||||
|
||||
await async_client.delete_by_query(
|
||||
index="test-index", body={"query": {"match_all": {}}}
|
||||
)
|
||||
|
||||
async for ok, item in helpers.async_streaming_bulk(
|
||||
async_client, sync_gen(), index="test-index", refresh=True
|
||||
):
|
||||
assert ok
|
||||
|
||||
assert 100 == (await async_client.count(index="test-index"))["count"]
|
||||
assert {"answer": 42} == (await async_client.get(index="test-index", id=42))[
|
||||
"_source"
|
||||
]
|
||||
|
||||
async def test_all_errors_from_chunk_are_raised_on_failure(self, async_client):
|
||||
await async_client.indices.create(
|
||||
"i",
|
||||
{
|
||||
"mappings": {"properties": {"a": {"type": "integer"}}},
|
||||
"settings": {"number_of_shards": 1, "number_of_replicas": 0},
|
||||
},
|
||||
)
|
||||
await async_client.cluster.health(wait_for_status="yellow")
|
||||
|
||||
try:
|
||||
async for ok, item in helpers.async_streaming_bulk(
|
||||
async_client, [{"a": "b"}, {"a": "c"}], index="i", raise_on_error=True
|
||||
):
|
||||
assert ok
|
||||
except helpers.BulkIndexError as e:
|
||||
assert 2 == len(e.errors)
|
||||
else:
|
||||
assert False, "exception should have been raised"
|
||||
|
||||
async def test_different_op_types(self, async_client):
|
||||
await async_client.index(index="i", id=45, body={})
|
||||
await async_client.index(index="i", id=42, body={})
|
||||
docs = [
|
||||
{"_index": "i", "_id": 47, "f": "v"},
|
||||
{"_op_type": "delete", "_index": "i", "_id": 45},
|
||||
{"_op_type": "update", "_index": "i", "_id": 42, "doc": {"answer": 42}},
|
||||
]
|
||||
async for ok, item in helpers.async_streaming_bulk(async_client, docs):
|
||||
assert ok
|
||||
|
||||
assert not await async_client.exists(index="i", id=45)
|
||||
assert {"answer": 42} == (await async_client.get(index="i", id=42))["_source"]
|
||||
assert {"f": "v"} == (await async_client.get(index="i", id=47))["_source"]
|
||||
|
||||
async def test_transport_error_can_becaught(self, async_client):
|
||||
failing_client = FailingBulkClient(async_client)
|
||||
docs = [
|
||||
{"_index": "i", "_id": 47, "f": "v"},
|
||||
{"_index": "i", "_id": 45, "f": "v"},
|
||||
{"_index": "i", "_id": 42, "f": "v"},
|
||||
]
|
||||
|
||||
results = [
|
||||
x
|
||||
async for x in helpers.async_streaming_bulk(
|
||||
failing_client,
|
||||
docs,
|
||||
raise_on_exception=False,
|
||||
raise_on_error=False,
|
||||
chunk_size=1,
|
||||
)
|
||||
]
|
||||
assert 3 == len(results)
|
||||
assert [True, False, True] == [r[0] for r in results]
|
||||
|
||||
exc = results[1][1]["index"].pop("exception")
|
||||
assert isinstance(exc, TransportError)
|
||||
assert 599 == exc.status_code
|
||||
assert {
|
||||
"index": {
|
||||
"_index": "i",
|
||||
"_id": 45,
|
||||
"data": {"f": "v"},
|
||||
"error": "TransportError(599, 'Error!')",
|
||||
"status": 599,
|
||||
}
|
||||
} == results[1][1]
|
||||
|
||||
async def test_rejected_documents_are_retried(self, async_client):
|
||||
failing_client = FailingBulkClient(
|
||||
async_client, fail_with=TransportError(429, "Rejected!", {})
|
||||
)
|
||||
docs = [
|
||||
{"_index": "i", "_id": 47, "f": "v"},
|
||||
{"_index": "i", "_id": 45, "f": "v"},
|
||||
{"_index": "i", "_id": 42, "f": "v"},
|
||||
]
|
||||
results = [
|
||||
x
|
||||
async for x in helpers.async_streaming_bulk(
|
||||
failing_client,
|
||||
docs,
|
||||
raise_on_exception=False,
|
||||
raise_on_error=False,
|
||||
chunk_size=1,
|
||||
max_retries=1,
|
||||
initial_backoff=0,
|
||||
)
|
||||
]
|
||||
assert 3 == len(results)
|
||||
assert [True, True, True] == [r[0] for r in results]
|
||||
await async_client.indices.refresh(index="i")
|
||||
res = await async_client.search(index="i")
|
||||
assert {"value": 3, "relation": "eq"} == res["hits"]["total"]
|
||||
assert 4 == failing_client._called
|
||||
|
||||
async def test_rejected_documents_are_retried_at_most_max_retries_times(
|
||||
self, async_client
|
||||
):
|
||||
failing_client = FailingBulkClient(
|
||||
async_client, fail_at=(1, 2), fail_with=TransportError(429, "Rejected!", {})
|
||||
)
|
||||
|
||||
docs = [
|
||||
{"_index": "i", "_id": 47, "f": "v"},
|
||||
{"_index": "i", "_id": 45, "f": "v"},
|
||||
{"_index": "i", "_id": 42, "f": "v"},
|
||||
]
|
||||
results = [
|
||||
x
|
||||
async for x in helpers.async_streaming_bulk(
|
||||
failing_client,
|
||||
docs,
|
||||
raise_on_exception=False,
|
||||
raise_on_error=False,
|
||||
chunk_size=1,
|
||||
max_retries=1,
|
||||
initial_backoff=0,
|
||||
)
|
||||
]
|
||||
assert 3 == len(results)
|
||||
assert [False, True, True] == [r[0] for r in results]
|
||||
await async_client.indices.refresh(index="i")
|
||||
res = await async_client.search(index="i")
|
||||
assert {"value": 2, "relation": "eq"} == res["hits"]["total"]
|
||||
assert 4 == failing_client._called
|
||||
|
||||
async def test_transport_error_is_raised_with_max_retries(self, async_client):
|
||||
failing_client = FailingBulkClient(
|
||||
async_client,
|
||||
fail_at=(1, 2, 3, 4),
|
||||
fail_with=TransportError(429, "Rejected!", {}),
|
||||
)
|
||||
|
||||
async def streaming_bulk():
|
||||
results = [
|
||||
x
|
||||
async for x in helpers.async_streaming_bulk(
|
||||
failing_client,
|
||||
[{"a": 42}, {"a": 39}],
|
||||
raise_on_exception=True,
|
||||
max_retries=3,
|
||||
initial_backoff=0,
|
||||
)
|
||||
]
|
||||
return results
|
||||
|
||||
with pytest.raises(TransportError):
|
||||
await streaming_bulk()
|
||||
assert 4 == failing_client._called
|
||||
|
||||
|
||||
class TestBulk(object):
|
||||
async def test_bulk_works_with_single_item(self, async_client):
|
||||
docs = [{"answer": 42, "_id": 1}]
|
||||
success, failed = await helpers.async_bulk(
|
||||
async_client, docs, index="test-index", refresh=True
|
||||
)
|
||||
|
||||
assert 1 == success
|
||||
assert not failed
|
||||
assert 1 == (await async_client.count(index="test-index"))["count"]
|
||||
assert {"answer": 42} == (await async_client.get(index="test-index", id=1))[
|
||||
"_source"
|
||||
]
|
||||
|
||||
async def test_all_documents_get_inserted(self, async_client):
|
||||
docs = [{"answer": x, "_id": x} for x in range(100)]
|
||||
success, failed = await helpers.async_bulk(
|
||||
async_client, docs, index="test-index", refresh=True
|
||||
)
|
||||
|
||||
assert 100 == success
|
||||
assert not failed
|
||||
assert 100 == (await async_client.count(index="test-index"))["count"]
|
||||
assert {"answer": 42} == (await async_client.get(index="test-index", id=42))[
|
||||
"_source"
|
||||
]
|
||||
|
||||
async def test_stats_only_reports_numbers(self, async_client):
|
||||
docs = [{"answer": x} for x in range(100)]
|
||||
success, failed = await helpers.async_bulk(
|
||||
async_client, docs, index="test-index", refresh=True, stats_only=True
|
||||
)
|
||||
|
||||
assert 100 == success
|
||||
assert 0 == failed
|
||||
assert 100 == (await async_client.count(index="test-index"))["count"]
|
||||
|
||||
async def test_errors_are_reported_correctly(self, async_client):
|
||||
await async_client.indices.create(
|
||||
"i",
|
||||
{
|
||||
"mappings": {"properties": {"a": {"type": "integer"}}},
|
||||
"settings": {"number_of_shards": 1, "number_of_replicas": 0},
|
||||
},
|
||||
)
|
||||
await async_client.cluster.health(wait_for_status="yellow")
|
||||
|
||||
success, failed = await helpers.async_bulk(
|
||||
async_client,
|
||||
[{"a": 42}, {"a": "c", "_id": 42}],
|
||||
index="i",
|
||||
raise_on_error=False,
|
||||
)
|
||||
assert 1 == success
|
||||
assert 1 == len(failed)
|
||||
error = failed[0]
|
||||
assert "42" == error["index"]["_id"]
|
||||
assert "i" == error["index"]["_index"]
|
||||
print(error["index"]["error"])
|
||||
assert "MapperParsingException" in repr(
|
||||
error["index"]["error"]
|
||||
) or "mapper_parsing_exception" in repr(error["index"]["error"])
|
||||
|
||||
async def test_error_is_raised(self, async_client):
|
||||
await async_client.indices.create(
|
||||
"i",
|
||||
{
|
||||
"mappings": {"properties": {"a": {"type": "integer"}}},
|
||||
"settings": {"number_of_shards": 1, "number_of_replicas": 0},
|
||||
},
|
||||
)
|
||||
await async_client.cluster.health(wait_for_status="yellow")
|
||||
|
||||
with pytest.raises(helpers.BulkIndexError):
|
||||
await helpers.async_bulk(async_client, [{"a": 42}, {"a": "c"}], index="i")
|
||||
|
||||
async def test_ignore_error_if_raised(self, async_client):
|
||||
# ignore the status code 400 in tuple
|
||||
await helpers.async_bulk(
|
||||
async_client, [{"a": 42}, {"a": "c"}], index="i", ignore_status=(400,)
|
||||
)
|
||||
|
||||
# ignore the status code 400 in list
|
||||
await helpers.async_bulk(
|
||||
async_client,
|
||||
[{"a": 42}, {"a": "c"}],
|
||||
index="i",
|
||||
ignore_status=[
|
||||
400,
|
||||
],
|
||||
)
|
||||
|
||||
# ignore the status code 400
|
||||
await helpers.async_bulk(
|
||||
async_client, [{"a": 42}, {"a": "c"}], index="i", ignore_status=400
|
||||
)
|
||||
|
||||
# ignore only the status code in the `ignore_status` argument
|
||||
with pytest.raises(helpers.BulkIndexError):
|
||||
await helpers.async_bulk(
|
||||
async_client, [{"a": 42}, {"a": "c"}], index="i", ignore_status=(444,)
|
||||
)
|
||||
|
||||
# ignore transport error exception
|
||||
failing_client = FailingBulkClient(async_client)
|
||||
await helpers.async_bulk(
|
||||
failing_client, [{"a": 42}], index="i", ignore_status=(599,)
|
||||
)
|
||||
|
||||
async def test_errors_are_collected_properly(self, async_client):
|
||||
await async_client.indices.create(
|
||||
"i",
|
||||
{
|
||||
"mappings": {"properties": {"a": {"type": "integer"}}},
|
||||
"settings": {"number_of_shards": 1, "number_of_replicas": 0},
|
||||
},
|
||||
)
|
||||
await async_client.cluster.health(wait_for_status="yellow")
|
||||
|
||||
success, failed = await helpers.async_bulk(
|
||||
async_client,
|
||||
[{"a": 42}, {"a": "c"}],
|
||||
index="i",
|
||||
stats_only=True,
|
||||
raise_on_error=False,
|
||||
)
|
||||
assert 1 == success
|
||||
assert 1 == failed
|
||||
|
||||
|
||||
class MockScroll:
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
async def __call__(self, *args, **kwargs):
|
||||
self.calls.append((args, kwargs))
|
||||
if len(self.calls) == 1:
|
||||
return {
|
||||
"_scroll_id": "dummy_id",
|
||||
"_shards": {"successful": 4, "total": 5, "skipped": 0},
|
||||
"hits": {"hits": [{"scroll_data": 42}]},
|
||||
}
|
||||
elif len(self.calls) == 2:
|
||||
return {
|
||||
"_scroll_id": "dummy_id",
|
||||
"_shards": {"successful": 4, "total": 5, "skipped": 0},
|
||||
"hits": {"hits": []},
|
||||
}
|
||||
else:
|
||||
raise Exception("no more responses")
|
||||
|
||||
|
||||
class MockResponse:
|
||||
def __init__(self, resp):
|
||||
self.resp = resp
|
||||
|
||||
async def __call__(self, *args, **kwargs):
|
||||
return self.resp
|
||||
|
||||
def __await__(self):
|
||||
return self().__await__()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
async def scan_teardown(async_client):
|
||||
yield
|
||||
await async_client.clear_scroll(scroll_id="_all")
|
||||
|
||||
|
||||
class TestScan(object):
|
||||
async def test_order_can_be_preserved(self, async_client, scan_teardown):
|
||||
bulk = []
|
||||
for x in range(100):
|
||||
bulk.append({"index": {"_index": "test_index", "_id": x}})
|
||||
bulk.append({"answer": x, "correct": x == 42})
|
||||
await async_client.bulk(bulk, refresh=True)
|
||||
|
||||
docs = [
|
||||
doc
|
||||
async for doc in helpers.async_scan(
|
||||
async_client,
|
||||
index="test_index",
|
||||
query={"sort": "answer"},
|
||||
preserve_order=True,
|
||||
)
|
||||
]
|
||||
|
||||
assert 100 == len(docs)
|
||||
assert list(map(str, range(100))) == list(d["_id"] for d in docs)
|
||||
assert list(range(100)) == list(d["_source"]["answer"] for d in docs)
|
||||
|
||||
async def test_all_documents_are_read(self, async_client, scan_teardown):
|
||||
bulk = []
|
||||
for x in range(100):
|
||||
bulk.append({"index": {"_index": "test_index", "_id": x}})
|
||||
bulk.append({"answer": x, "correct": x == 42})
|
||||
await async_client.bulk(bulk, refresh=True)
|
||||
|
||||
docs = [
|
||||
x
|
||||
async for x in helpers.async_scan(async_client, index="test_index", size=2)
|
||||
]
|
||||
|
||||
assert 100 == len(docs)
|
||||
assert set(map(str, range(100))) == set(d["_id"] for d in docs)
|
||||
assert set(range(100)) == set(d["_source"]["answer"] for d in docs)
|
||||
|
||||
async def test_scroll_error(self, async_client, scan_teardown):
|
||||
bulk = []
|
||||
for x in range(4):
|
||||
bulk.append({"index": {"_index": "test_index"}})
|
||||
bulk.append({"value": x})
|
||||
await async_client.bulk(bulk, refresh=True)
|
||||
|
||||
with patch.object(async_client, "scroll", MockScroll()):
|
||||
data = [
|
||||
x
|
||||
async for x in helpers.async_scan(
|
||||
async_client,
|
||||
index="test_index",
|
||||
size=2,
|
||||
raise_on_error=False,
|
||||
clear_scroll=False,
|
||||
)
|
||||
]
|
||||
assert len(data) == 3
|
||||
assert data[-1] == {"scroll_data": 42}
|
||||
|
||||
with patch.object(async_client, "scroll", MockScroll()):
|
||||
with pytest.raises(ScanError):
|
||||
data = [
|
||||
x
|
||||
async for x in helpers.async_scan(
|
||||
async_client,
|
||||
index="test_index",
|
||||
size=2,
|
||||
raise_on_error=True,
|
||||
clear_scroll=False,
|
||||
)
|
||||
]
|
||||
assert len(data) == 3
|
||||
assert data[-1] == {"scroll_data": 42}
|
||||
|
||||
async def test_initial_search_error(self, async_client, scan_teardown):
|
||||
with patch.object(async_client, "clear_scroll", new_callable=AsyncMock):
|
||||
with patch.object(
|
||||
async_client,
|
||||
"search",
|
||||
MockResponse(
|
||||
{
|
||||
"_scroll_id": "dummy_id",
|
||||
"_shards": {"successful": 4, "total": 5, "skipped": 0},
|
||||
"hits": {"hits": [{"search_data": 1}]},
|
||||
}
|
||||
),
|
||||
):
|
||||
with patch.object(async_client, "scroll", MockScroll()):
|
||||
|
||||
data = [
|
||||
x
|
||||
async for x in helpers.async_scan(
|
||||
async_client,
|
||||
index="test_index",
|
||||
size=2,
|
||||
raise_on_error=False,
|
||||
)
|
||||
]
|
||||
assert data == [{"search_data": 1}, {"scroll_data": 42}]
|
||||
|
||||
with patch.object(
|
||||
async_client,
|
||||
"search",
|
||||
MockResponse(
|
||||
{
|
||||
"_scroll_id": "dummy_id",
|
||||
"_shards": {"successful": 4, "total": 5, "skipped": 0},
|
||||
"hits": {"hits": [{"search_data": 1}]},
|
||||
}
|
||||
),
|
||||
):
|
||||
with patch.object(async_client, "scroll", MockScroll()) as mock_scroll:
|
||||
|
||||
with pytest.raises(ScanError):
|
||||
data = [
|
||||
x
|
||||
async for x in helpers.async_scan(
|
||||
async_client,
|
||||
index="test_index",
|
||||
size=2,
|
||||
raise_on_error=True,
|
||||
)
|
||||
]
|
||||
assert data == [{"search_data": 1}]
|
||||
assert mock_scroll.calls == []
|
||||
|
||||
async def test_no_scroll_id_fast_route(self, async_client, scan_teardown):
|
||||
with patch.object(async_client, "search", MockResponse({"no": "_scroll_id"})):
|
||||
with patch.object(async_client, "scroll") as scroll_mock:
|
||||
with patch.object(async_client, "clear_scroll") as clear_mock:
|
||||
data = [
|
||||
x
|
||||
async for x in helpers.async_scan(
|
||||
async_client, index="test_index"
|
||||
)
|
||||
]
|
||||
|
||||
assert data == []
|
||||
scroll_mock.assert_not_called()
|
||||
clear_mock.assert_not_called()
|
||||
|
||||
@patch("opensearchpy._async.helpers.logger")
|
||||
async def test_logger(self, logger_mock, async_client, scan_teardown):
|
||||
bulk = []
|
||||
for x in range(4):
|
||||
bulk.append({"index": {"_index": "test_index"}})
|
||||
bulk.append({"value": x})
|
||||
await async_client.bulk(bulk, refresh=True)
|
||||
|
||||
with patch.object(async_client, "scroll", MockScroll()):
|
||||
_ = [
|
||||
x
|
||||
async for x in helpers.async_scan(
|
||||
async_client,
|
||||
index="test_index",
|
||||
size=2,
|
||||
raise_on_error=False,
|
||||
clear_scroll=False,
|
||||
)
|
||||
]
|
||||
logger_mock.warning.assert_called()
|
||||
|
||||
with patch.object(async_client, "scroll", MockScroll()):
|
||||
try:
|
||||
_ = [
|
||||
x
|
||||
async for x in helpers.async_scan(
|
||||
async_client,
|
||||
index="test_index",
|
||||
size=2,
|
||||
raise_on_error=True,
|
||||
clear_scroll=False,
|
||||
)
|
||||
]
|
||||
except ScanError:
|
||||
pass
|
||||
logger_mock.warning.assert_called_with(
|
||||
"Scroll request has only succeeded on %d (+%d skipped) shards out of %d.",
|
||||
4,
|
||||
0,
|
||||
5,
|
||||
)
|
||||
|
||||
async def test_clear_scroll(self, async_client, scan_teardown):
|
||||
bulk = []
|
||||
for x in range(4):
|
||||
bulk.append({"index": {"_index": "test_index"}})
|
||||
bulk.append({"value": x})
|
||||
await async_client.bulk(bulk, refresh=True)
|
||||
|
||||
with patch.object(
|
||||
async_client, "clear_scroll", wraps=async_client.clear_scroll
|
||||
) as spy:
|
||||
_ = [
|
||||
x
|
||||
async for x in helpers.async_scan(
|
||||
async_client, index="test_index", size=2
|
||||
)
|
||||
]
|
||||
spy.assert_called_once()
|
||||
|
||||
spy.reset_mock()
|
||||
_ = [
|
||||
x
|
||||
async for x in helpers.async_scan(
|
||||
async_client, index="test_index", size=2, clear_scroll=True
|
||||
)
|
||||
]
|
||||
spy.assert_called_once()
|
||||
|
||||
spy.reset_mock()
|
||||
_ = [
|
||||
x
|
||||
async for x in helpers.async_scan(
|
||||
async_client, index="test_index", size=2, clear_scroll=False
|
||||
)
|
||||
]
|
||||
spy.assert_not_called()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"kwargs",
|
||||
[
|
||||
{"api_key": ("name", "value")},
|
||||
{"http_auth": ("username", "password")},
|
||||
{"headers": {"custom", "header"}},
|
||||
],
|
||||
)
|
||||
async def test_scan_auth_kwargs_forwarded(
|
||||
self, async_client, scan_teardown, kwargs
|
||||
):
|
||||
((key, val),) = kwargs.items()
|
||||
|
||||
with patch.object(
|
||||
async_client,
|
||||
"search",
|
||||
return_value=MockResponse(
|
||||
{
|
||||
"_scroll_id": "scroll_id",
|
||||
"_shards": {"successful": 5, "total": 5, "skipped": 0},
|
||||
"hits": {"hits": [{"search_data": 1}]},
|
||||
}
|
||||
),
|
||||
) as search_mock:
|
||||
with patch.object(
|
||||
async_client,
|
||||
"scroll",
|
||||
return_value=MockResponse(
|
||||
{
|
||||
"_scroll_id": "scroll_id",
|
||||
"_shards": {"successful": 5, "total": 5, "skipped": 0},
|
||||
"hits": {"hits": []},
|
||||
}
|
||||
),
|
||||
) as scroll_mock:
|
||||
with patch.object(
|
||||
async_client, "clear_scroll", return_value=MockResponse({})
|
||||
) as clear_mock:
|
||||
data = [
|
||||
x
|
||||
async for x in helpers.async_scan(
|
||||
async_client, index="test_index", **kwargs
|
||||
)
|
||||
]
|
||||
|
||||
assert data == [{"search_data": 1}]
|
||||
|
||||
for api_mock in (search_mock, scroll_mock, clear_mock):
|
||||
assert api_mock.call_args[1][key] == val
|
||||
|
||||
async def test_scan_auth_kwargs_favor_scroll_kwargs_option(
|
||||
self, async_client, scan_teardown
|
||||
):
|
||||
with patch.object(
|
||||
async_client,
|
||||
"search",
|
||||
return_value=MockResponse(
|
||||
{
|
||||
"_scroll_id": "scroll_id",
|
||||
"_shards": {"successful": 5, "total": 5, "skipped": 0},
|
||||
"hits": {"hits": [{"search_data": 1}]},
|
||||
}
|
||||
),
|
||||
):
|
||||
with patch.object(
|
||||
async_client,
|
||||
"scroll",
|
||||
return_value=MockResponse(
|
||||
{
|
||||
"_scroll_id": "scroll_id",
|
||||
"_shards": {"successful": 5, "total": 5, "skipped": 0},
|
||||
"hits": {"hits": []},
|
||||
}
|
||||
),
|
||||
):
|
||||
with patch.object(
|
||||
async_client, "clear_scroll", return_value=MockResponse({})
|
||||
):
|
||||
data = [
|
||||
x
|
||||
async for x in helpers.async_scan(
|
||||
async_client,
|
||||
index="test_index",
|
||||
headers={"not scroll": "kwargs"},
|
||||
scroll_kwargs={
|
||||
"headers": {"scroll": "kwargs"},
|
||||
"sort": "asc",
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
assert data == [{"search_data": 1}]
|
||||
|
||||
# Assert that we see 'scroll_kwargs' options used instead of 'kwargs'
|
||||
assert async_client.scroll.call_args[1]["headers"] == {
|
||||
"scroll": "kwargs"
|
||||
}
|
||||
assert async_client.scroll.call_args[1]["sort"] == "asc"
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
async def reindex_setup(async_client):
|
||||
bulk = []
|
||||
for x in range(100):
|
||||
bulk.append({"index": {"_index": "test_index", "_id": x}})
|
||||
bulk.append(
|
||||
{
|
||||
"answer": x,
|
||||
"correct": x == 42,
|
||||
"type": "answers" if x % 2 == 0 else "questions",
|
||||
}
|
||||
)
|
||||
await async_client.bulk(bulk, refresh=True)
|
||||
yield
|
||||
|
||||
|
||||
class TestReindex(object):
|
||||
async def test_reindex_passes_kwargs_to_scan_and_bulk(
|
||||
self, async_client, reindex_setup
|
||||
):
|
||||
await helpers.async_reindex(
|
||||
async_client,
|
||||
"test_index",
|
||||
"prod_index",
|
||||
scan_kwargs={"q": "type:answers"},
|
||||
bulk_kwargs={"refresh": True},
|
||||
)
|
||||
|
||||
assert await async_client.indices.exists("prod_index")
|
||||
assert (
|
||||
50
|
||||
== (await async_client.count(index="prod_index", q="type:answers"))["count"]
|
||||
)
|
||||
|
||||
assert {"answer": 42, "correct": True, "type": "answers"} == (
|
||||
await async_client.get(index="prod_index", id=42)
|
||||
)["_source"]
|
||||
|
||||
async def test_reindex_accepts_a_query(self, async_client, reindex_setup):
|
||||
await helpers.async_reindex(
|
||||
async_client,
|
||||
"test_index",
|
||||
"prod_index",
|
||||
query={"query": {"bool": {"filter": {"term": {"type": "answers"}}}}},
|
||||
)
|
||||
await async_client.indices.refresh()
|
||||
|
||||
assert await async_client.indices.exists("prod_index")
|
||||
assert (
|
||||
50
|
||||
== (await async_client.count(index="prod_index", q="type:answers"))["count"]
|
||||
)
|
||||
|
||||
assert {"answer": 42, "correct": True, "type": "answers"} == (
|
||||
await async_client.get(index="prod_index", id=42)
|
||||
)["_source"]
|
||||
|
||||
async def test_all_documents_get_moved(self, async_client, reindex_setup):
|
||||
await helpers.async_reindex(async_client, "test_index", "prod_index")
|
||||
await async_client.indices.refresh()
|
||||
|
||||
assert await async_client.indices.exists("prod_index")
|
||||
assert (
|
||||
50
|
||||
== (await async_client.count(index="prod_index", q="type:questions"))[
|
||||
"count"
|
||||
]
|
||||
)
|
||||
assert (
|
||||
50
|
||||
== (await async_client.count(index="prod_index", q="type:answers"))["count"]
|
||||
)
|
||||
|
||||
assert {"answer": 42, "correct": True, "type": "answers"} == (
|
||||
await async_client.get(index="prod_index", id=42)
|
||||
)["_source"]
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
async def parent_reindex_setup(async_client):
|
||||
body = {
|
||||
"settings": {"number_of_shards": 1, "number_of_replicas": 0},
|
||||
"mappings": {
|
||||
"properties": {
|
||||
"question_answer": {
|
||||
"type": "join",
|
||||
"relations": {"question": "answer"},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
await async_client.indices.create(index="test-index", body=body)
|
||||
await async_client.indices.create(index="real-index", body=body)
|
||||
|
||||
await async_client.index(
|
||||
index="test-index", id=42, body={"question_answer": "question"}
|
||||
)
|
||||
await async_client.index(
|
||||
index="test-index",
|
||||
id=47,
|
||||
routing=42,
|
||||
body={"some": "data", "question_answer": {"name": "answer", "parent": 42}},
|
||||
)
|
||||
await async_client.indices.refresh(index="test-index")
|
||||
|
||||
|
||||
class TestParentChildReindex:
|
||||
async def test_children_are_reindexed_correctly(
|
||||
self, async_client, parent_reindex_setup
|
||||
):
|
||||
await helpers.async_reindex(async_client, "test-index", "real-index")
|
||||
|
||||
q = await async_client.get(index="real-index", id=42)
|
||||
assert {
|
||||
"_id": "42",
|
||||
"_index": "real-index",
|
||||
"_primary_term": 1,
|
||||
"_seq_no": 0,
|
||||
"_source": {"question_answer": "question"},
|
||||
"_type": "_doc",
|
||||
"_version": 1,
|
||||
"found": True,
|
||||
} == q
|
||||
|
||||
q = await async_client.get(index="test-index", id=47, routing=42)
|
||||
assert {
|
||||
"_routing": "42",
|
||||
"_id": "47",
|
||||
"_index": "test-index",
|
||||
"_primary_term": 1,
|
||||
"_seq_no": 1,
|
||||
"_source": {
|
||||
"some": "data",
|
||||
"question_answer": {"name": "answer", "parent": 42},
|
||||
},
|
||||
"_type": "_doc",
|
||||
"_version": 1,
|
||||
"found": True,
|
||||
} == q
|
||||
@@ -0,0 +1,233 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Dynamically generated set of TestCases based on set of yaml files decribing
|
||||
some integration tests. These files are shared among all official OpenSearch
|
||||
clients.
|
||||
"""
|
||||
import inspect
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
|
||||
from opensearchpy import OpenSearchWarning
|
||||
from opensearchpy.helpers.test import _get_version
|
||||
|
||||
from ...test_server.test_rest_api_spec import (
|
||||
IMPLEMENTED_FEATURES,
|
||||
PARAMS_RENAMES,
|
||||
RUN_ASYNC_REST_API_TESTS,
|
||||
YAML_TEST_SPECS,
|
||||
YamlRunner,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
OPENSEARCH_VERSION = None
|
||||
|
||||
|
||||
async def await_if_coro(x):
|
||||
if inspect.iscoroutine(x):
|
||||
return await x
|
||||
return x
|
||||
|
||||
|
||||
class AsyncYamlRunner(YamlRunner):
|
||||
async def setup(self):
|
||||
# Pull skips from individual tests to not do unnecessary setup.
|
||||
skip_code = []
|
||||
for action in self._run_code:
|
||||
assert len(action) == 1
|
||||
action_type, _ = list(action.items())[0]
|
||||
if action_type == "skip":
|
||||
skip_code.append(action)
|
||||
else:
|
||||
break
|
||||
|
||||
if self._setup_code or skip_code:
|
||||
self.section("setup")
|
||||
if skip_code:
|
||||
await self.run_code(skip_code)
|
||||
if self._setup_code:
|
||||
await self.run_code(self._setup_code)
|
||||
|
||||
async def teardown(self):
|
||||
if self._teardown_code:
|
||||
self.section("teardown")
|
||||
await self.run_code(self._teardown_code)
|
||||
|
||||
async def opensearch_version(self):
|
||||
global OPENSEARCH_VERSION
|
||||
if OPENSEARCH_VERSION is None:
|
||||
version_string = (await self.client.info())["version"]["number"]
|
||||
if "." not in version_string:
|
||||
return ()
|
||||
version = version_string.strip().split(".")
|
||||
OPENSEARCH_VERSION = tuple(int(v) if v.isdigit() else 999 for v in version)
|
||||
return OPENSEARCH_VERSION
|
||||
|
||||
def section(self, name):
|
||||
print(("=" * 10) + " " + name + " " + ("=" * 10))
|
||||
|
||||
async def run(self):
|
||||
try:
|
||||
await self.setup()
|
||||
self.section("test")
|
||||
await self.run_code(self._run_code)
|
||||
finally:
|
||||
try:
|
||||
await self.teardown()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def run_code(self, test):
|
||||
"""Execute an instruction based on it's type."""
|
||||
for action in test:
|
||||
assert len(action) == 1
|
||||
action_type, action = list(action.items())[0]
|
||||
print(action_type, action)
|
||||
|
||||
if hasattr(self, "run_" + action_type):
|
||||
await await_if_coro(getattr(self, "run_" + action_type)(action))
|
||||
else:
|
||||
raise RuntimeError("Invalid action type %r" % (action_type,))
|
||||
|
||||
async def run_do(self, action):
|
||||
api = self.client
|
||||
headers = action.pop("headers", None)
|
||||
catch = action.pop("catch", None)
|
||||
warn = action.pop("warnings", ())
|
||||
allowed_warnings = action.pop("allowed_warnings", ())
|
||||
assert len(action) == 1
|
||||
|
||||
# Remove the x_pack_rest_user authentication
|
||||
# if it's given via headers. We're already authenticated
|
||||
# via the 'elastic' user.
|
||||
if (
|
||||
headers
|
||||
and headers.get("Authorization", None)
|
||||
== "Basic eF9wYWNrX3Jlc3RfdXNlcjp4LXBhY2stdGVzdC1wYXNzd29yZA=="
|
||||
):
|
||||
headers.pop("Authorization")
|
||||
|
||||
method, args = list(action.items())[0]
|
||||
args["headers"] = headers
|
||||
|
||||
# locate api endpoint
|
||||
for m in method.split("."):
|
||||
assert hasattr(api, m)
|
||||
api = getattr(api, m)
|
||||
|
||||
# some parameters had to be renamed to not clash with python builtins,
|
||||
# compensate
|
||||
for k in PARAMS_RENAMES:
|
||||
if k in args:
|
||||
args[PARAMS_RENAMES[k]] = args.pop(k)
|
||||
|
||||
# resolve vars
|
||||
for k in args:
|
||||
args[k] = self._resolve(args[k])
|
||||
|
||||
warnings.simplefilter("always", category=OpenSearchWarning)
|
||||
with warnings.catch_warnings(record=True) as caught_warnings:
|
||||
try:
|
||||
self.last_response = await api(**args)
|
||||
except Exception as e:
|
||||
if not catch:
|
||||
raise
|
||||
self.run_catch(catch, e)
|
||||
else:
|
||||
if catch:
|
||||
raise AssertionError(
|
||||
"Failed to catch %r in %r." % (catch, self.last_response)
|
||||
)
|
||||
|
||||
# Filter out warnings raised by other components.
|
||||
caught_warnings = [
|
||||
str(w.message)
|
||||
for w in caught_warnings
|
||||
if w.category == OpenSearchWarning
|
||||
and str(w.message) not in allowed_warnings
|
||||
]
|
||||
|
||||
# This warning can show up in many places but isn't accounted for
|
||||
# in tests, so we remove it to make sure things pass.
|
||||
include_type_name_warning = (
|
||||
"[types removal] Using include_type_name in create index requests is deprecated. "
|
||||
"The parameter will be removed in the next major version."
|
||||
)
|
||||
if (
|
||||
include_type_name_warning in caught_warnings
|
||||
and include_type_name_warning not in warn
|
||||
):
|
||||
caught_warnings.remove(include_type_name_warning)
|
||||
|
||||
# Sorting removes the issue with order raised. We only care about
|
||||
# if all warnings are raised in the single API call.
|
||||
if warn and sorted(warn) != sorted(caught_warnings):
|
||||
raise AssertionError(
|
||||
"Expected warnings not equal to actual warnings: expected=%r actual=%r"
|
||||
% (warn, caught_warnings)
|
||||
)
|
||||
|
||||
async def run_skip(self, skip):
|
||||
if "features" in skip:
|
||||
features = skip["features"]
|
||||
if not isinstance(features, (tuple, list)):
|
||||
features = [features]
|
||||
for feature in features:
|
||||
if feature in IMPLEMENTED_FEATURES:
|
||||
continue
|
||||
pytest.skip("feature '%s' is not supported" % feature)
|
||||
|
||||
if "version" in skip:
|
||||
version, reason = skip["version"], skip["reason"]
|
||||
if version == "all":
|
||||
pytest.skip(reason)
|
||||
min_version, max_version = version.split("-")
|
||||
min_version = _get_version(min_version) or (0,)
|
||||
max_version = _get_version(max_version) or (999,)
|
||||
if min_version <= (await self.opensearch_version()) <= max_version:
|
||||
pytest.skip(reason)
|
||||
|
||||
async def _feature_enabled(self, name):
|
||||
return False
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def async_runner(async_client):
|
||||
return AsyncYamlRunner(async_client)
|
||||
|
||||
|
||||
if RUN_ASYNC_REST_API_TESTS:
|
||||
|
||||
@pytest.mark.parametrize("test_spec", YAML_TEST_SPECS)
|
||||
async def test_rest_api_spec(test_spec, async_runner):
|
||||
if test_spec.get("skip", False):
|
||||
pytest.skip("Manually skipped in 'SKIP_TESTS'")
|
||||
async_runner.use_spec(test_spec)
|
||||
await async_runner.run()
|
||||
@@ -0,0 +1,549 @@
|
||||
# -*- 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.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from mock import patch
|
||||
|
||||
from opensearchpy import AsyncTransport
|
||||
from opensearchpy.connection import Connection
|
||||
from opensearchpy.connection_pool import DummyConnectionPool
|
||||
from opensearchpy.exceptions import ConnectionError, TransportError
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
class DummyConnection(Connection):
|
||||
def __init__(self, **kwargs):
|
||||
self.exception = kwargs.pop("exception", None)
|
||||
self.status, self.data = kwargs.pop("status", 200), kwargs.pop("data", "{}")
|
||||
self.headers = kwargs.pop("headers", {})
|
||||
self.delay = kwargs.pop("delay", 0)
|
||||
self.calls = []
|
||||
self.closed = False
|
||||
super(DummyConnection, self).__init__(**kwargs)
|
||||
|
||||
async def perform_request(self, *args, **kwargs):
|
||||
if self.closed:
|
||||
raise RuntimeError("This connection is closed")
|
||||
if self.delay:
|
||||
await asyncio.sleep(self.delay)
|
||||
self.calls.append((args, kwargs))
|
||||
if self.exception:
|
||||
raise self.exception
|
||||
return self.status, self.headers, self.data
|
||||
|
||||
async def close(self):
|
||||
if self.closed:
|
||||
raise RuntimeError("This connection is already closed")
|
||||
self.closed = True
|
||||
|
||||
|
||||
CLUSTER_NODES = """{
|
||||
"_nodes" : {
|
||||
"total" : 1,
|
||||
"successful" : 1,
|
||||
"failed" : 0
|
||||
},
|
||||
"cluster_name" : "opensearch",
|
||||
"nodes" : {
|
||||
"SRZpKFZdQguhhvifmN6UVA" : {
|
||||
"name" : "SRZpKFZ",
|
||||
"transport_address" : "127.0.0.1:9300",
|
||||
"host" : "127.0.0.1",
|
||||
"ip" : "127.0.0.1",
|
||||
"version" : "5.0.0",
|
||||
"build_hash" : "253032b",
|
||||
"roles" : [ "master", "data", "ingest" ],
|
||||
"http" : {
|
||||
"bound_address" : [ "[fe80::1]:9200", "[::1]:9200", "127.0.0.1:9200" ],
|
||||
"publish_address" : "1.1.1.1:123",
|
||||
"max_content_length_in_bytes" : 104857600
|
||||
}
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
CLUSTER_NODES_7x_PUBLISH_HOST = """{
|
||||
"_nodes" : {
|
||||
"total" : 1,
|
||||
"successful" : 1,
|
||||
"failed" : 0
|
||||
},
|
||||
"cluster_name" : "opensearch",
|
||||
"nodes" : {
|
||||
"SRZpKFZdQguhhvifmN6UVA" : {
|
||||
"name" : "SRZpKFZ",
|
||||
"transport_address" : "127.0.0.1:9300",
|
||||
"host" : "127.0.0.1",
|
||||
"ip" : "127.0.0.1",
|
||||
"version" : "5.0.0",
|
||||
"build_hash" : "253032b",
|
||||
"roles" : [ "master", "data", "ingest" ],
|
||||
"http" : {
|
||||
"bound_address" : [ "[fe80::1]:9200", "[::1]:9200", "127.0.0.1:9200" ],
|
||||
"publish_address" : "somehost.tld/1.1.1.1:123",
|
||||
"max_content_length_in_bytes" : 104857600
|
||||
}
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
|
||||
class TestTransport:
|
||||
async def test_single_connection_uses_dummy_connection_pool(self):
|
||||
t = AsyncTransport([{}])
|
||||
await t._async_call()
|
||||
assert isinstance(t.connection_pool, DummyConnectionPool)
|
||||
t = AsyncTransport([{"host": "localhost"}])
|
||||
await t._async_call()
|
||||
assert isinstance(t.connection_pool, DummyConnectionPool)
|
||||
|
||||
async def test_request_timeout_extracted_from_params_and_passed(self):
|
||||
t = AsyncTransport([{}], connection_class=DummyConnection)
|
||||
|
||||
await t.perform_request("GET", "/", params={"request_timeout": 42})
|
||||
assert 1 == len(t.get_connection().calls)
|
||||
assert ("GET", "/", {}, None) == t.get_connection().calls[0][0]
|
||||
assert {
|
||||
"timeout": 42,
|
||||
"ignore": (),
|
||||
"headers": None,
|
||||
} == t.get_connection().calls[0][1]
|
||||
|
||||
async def test_opaque_id(self):
|
||||
t = AsyncTransport([{}], opaque_id="app-1", connection_class=DummyConnection)
|
||||
|
||||
await t.perform_request("GET", "/")
|
||||
assert 1 == len(t.get_connection().calls)
|
||||
assert ("GET", "/", None, None) == t.get_connection().calls[0][0]
|
||||
assert {
|
||||
"timeout": None,
|
||||
"ignore": (),
|
||||
"headers": None,
|
||||
} == t.get_connection().calls[0][1]
|
||||
|
||||
# Now try with an 'x-opaque-id' set on perform_request().
|
||||
await t.perform_request("GET", "/", headers={"x-opaque-id": "request-1"})
|
||||
assert 2 == len(t.get_connection().calls)
|
||||
assert ("GET", "/", None, None) == t.get_connection().calls[1][0]
|
||||
assert {
|
||||
"timeout": None,
|
||||
"ignore": (),
|
||||
"headers": {"x-opaque-id": "request-1"},
|
||||
} == t.get_connection().calls[1][1]
|
||||
|
||||
async def test_request_with_custom_user_agent_header(self):
|
||||
t = AsyncTransport([{}], connection_class=DummyConnection)
|
||||
|
||||
await t.perform_request(
|
||||
"GET", "/", headers={"user-agent": "my-custom-value/1.2.3"}
|
||||
)
|
||||
assert 1 == len(t.get_connection().calls)
|
||||
assert {
|
||||
"timeout": None,
|
||||
"ignore": (),
|
||||
"headers": {"user-agent": "my-custom-value/1.2.3"},
|
||||
} == t.get_connection().calls[0][1]
|
||||
|
||||
async def test_send_get_body_as_source(self):
|
||||
t = AsyncTransport(
|
||||
[{}], send_get_body_as="source", connection_class=DummyConnection
|
||||
)
|
||||
|
||||
await t.perform_request("GET", "/", body={})
|
||||
assert 1 == len(t.get_connection().calls)
|
||||
assert ("GET", "/", {"source": "{}"}, None) == t.get_connection().calls[0][0]
|
||||
|
||||
async def test_send_get_body_as_post(self):
|
||||
t = AsyncTransport(
|
||||
[{}], send_get_body_as="POST", connection_class=DummyConnection
|
||||
)
|
||||
|
||||
await t.perform_request("GET", "/", body={})
|
||||
assert 1 == len(t.get_connection().calls)
|
||||
assert ("POST", "/", None, b"{}") == t.get_connection().calls[0][0]
|
||||
|
||||
async def test_body_gets_encoded_into_bytes(self):
|
||||
t = AsyncTransport([{}], connection_class=DummyConnection)
|
||||
|
||||
await t.perform_request("GET", "/", body="你好")
|
||||
assert 1 == len(t.get_connection().calls)
|
||||
assert (
|
||||
"GET",
|
||||
"/",
|
||||
None,
|
||||
b"\xe4\xbd\xa0\xe5\xa5\xbd",
|
||||
) == t.get_connection().calls[0][0]
|
||||
|
||||
async def test_body_bytes_get_passed_untouched(self):
|
||||
t = AsyncTransport([{}], connection_class=DummyConnection)
|
||||
|
||||
body = b"\xe4\xbd\xa0\xe5\xa5\xbd"
|
||||
await t.perform_request("GET", "/", body=body)
|
||||
assert 1 == len(t.get_connection().calls)
|
||||
assert ("GET", "/", None, body) == t.get_connection().calls[0][0]
|
||||
|
||||
async def test_body_surrogates_replaced_encoded_into_bytes(self):
|
||||
t = AsyncTransport([{}], connection_class=DummyConnection)
|
||||
|
||||
await t.perform_request("GET", "/", body="你好\uda6a")
|
||||
assert 1 == len(t.get_connection().calls)
|
||||
assert (
|
||||
"GET",
|
||||
"/",
|
||||
None,
|
||||
b"\xe4\xbd\xa0\xe5\xa5\xbd\xed\xa9\xaa",
|
||||
) == t.get_connection().calls[0][0]
|
||||
|
||||
async def test_kwargs_passed_on_to_connections(self):
|
||||
t = AsyncTransport([{"host": "google.com"}], port=123)
|
||||
await t._async_call()
|
||||
assert 1 == len(t.connection_pool.connections)
|
||||
assert "http://google.com:123" == t.connection_pool.connections[0].host
|
||||
|
||||
async def test_kwargs_passed_on_to_connection_pool(self):
|
||||
dt = object()
|
||||
t = AsyncTransport([{}, {}], dead_timeout=dt)
|
||||
await t._async_call()
|
||||
assert dt is t.connection_pool.dead_timeout
|
||||
|
||||
async def test_custom_connection_class(self):
|
||||
class MyConnection(object):
|
||||
def __init__(self, **kwargs):
|
||||
self.kwargs = kwargs
|
||||
|
||||
t = AsyncTransport([{}], connection_class=MyConnection)
|
||||
await t._async_call()
|
||||
assert 1 == len(t.connection_pool.connections)
|
||||
assert isinstance(t.connection_pool.connections[0], MyConnection)
|
||||
|
||||
def test_add_connection(self):
|
||||
t = AsyncTransport([{}], randomize_hosts=False)
|
||||
t.add_connection({"host": "google.com", "port": 1234})
|
||||
|
||||
assert 2 == len(t.connection_pool.connections)
|
||||
assert "http://google.com:1234" == t.connection_pool.connections[1].host
|
||||
|
||||
async def test_request_will_fail_after_X_retries(self):
|
||||
t = AsyncTransport(
|
||||
[{"exception": ConnectionError("abandon ship")}],
|
||||
connection_class=DummyConnection,
|
||||
)
|
||||
|
||||
connection_error = False
|
||||
try:
|
||||
await t.perform_request("GET", "/")
|
||||
except ConnectionError:
|
||||
connection_error = True
|
||||
|
||||
assert connection_error
|
||||
assert 4 == len(t.get_connection().calls)
|
||||
|
||||
async def test_failed_connection_will_be_marked_as_dead(self):
|
||||
t = AsyncTransport(
|
||||
[{"exception": ConnectionError("abandon ship")}] * 2,
|
||||
connection_class=DummyConnection,
|
||||
)
|
||||
|
||||
connection_error = False
|
||||
try:
|
||||
await t.perform_request("GET", "/")
|
||||
except ConnectionError:
|
||||
connection_error = True
|
||||
|
||||
assert connection_error
|
||||
assert 0 == len(t.connection_pool.connections)
|
||||
|
||||
async def test_resurrected_connection_will_be_marked_as_live_on_success(self):
|
||||
for method in ("GET", "HEAD"):
|
||||
t = AsyncTransport([{}, {}], connection_class=DummyConnection)
|
||||
await t._async_call()
|
||||
con1 = t.connection_pool.get_connection()
|
||||
con2 = t.connection_pool.get_connection()
|
||||
t.connection_pool.mark_dead(con1)
|
||||
t.connection_pool.mark_dead(con2)
|
||||
|
||||
await t.perform_request(method, "/")
|
||||
assert 1 == len(t.connection_pool.connections)
|
||||
assert 1 == len(t.connection_pool.dead_count)
|
||||
|
||||
async def test_sniff_will_use_seed_connections(self):
|
||||
t = AsyncTransport([{"data": CLUSTER_NODES}], connection_class=DummyConnection)
|
||||
await t._async_call()
|
||||
t.set_connections([{"data": "invalid"}])
|
||||
|
||||
await t.sniff_hosts()
|
||||
assert 1 == len(t.connection_pool.connections)
|
||||
assert "http://1.1.1.1:123" == t.get_connection().host
|
||||
|
||||
async def test_sniff_on_start_fetches_and_uses_nodes_list(self):
|
||||
t = AsyncTransport(
|
||||
[{"data": CLUSTER_NODES}],
|
||||
connection_class=DummyConnection,
|
||||
sniff_on_start=True,
|
||||
)
|
||||
await t._async_call()
|
||||
await t.sniffing_task # Need to wait for the sniffing task to complete
|
||||
|
||||
assert 1 == len(t.connection_pool.connections)
|
||||
assert "http://1.1.1.1:123" == t.get_connection().host
|
||||
|
||||
async def test_sniff_on_start_ignores_sniff_timeout(self):
|
||||
t = AsyncTransport(
|
||||
[{"data": CLUSTER_NODES}],
|
||||
connection_class=DummyConnection,
|
||||
sniff_on_start=True,
|
||||
sniff_timeout=12,
|
||||
)
|
||||
await t._async_call()
|
||||
await t.sniffing_task # Need to wait for the sniffing task to complete
|
||||
|
||||
assert (("GET", "/_nodes/_all/http"), {"timeout": None}) == t.seed_connections[
|
||||
0
|
||||
].calls[0]
|
||||
|
||||
async def test_sniff_uses_sniff_timeout(self):
|
||||
t = AsyncTransport(
|
||||
[{"data": CLUSTER_NODES}],
|
||||
connection_class=DummyConnection,
|
||||
sniff_timeout=42,
|
||||
)
|
||||
await t._async_call()
|
||||
await t.sniff_hosts()
|
||||
|
||||
assert (("GET", "/_nodes/_all/http"), {"timeout": 42}) == t.seed_connections[
|
||||
0
|
||||
].calls[0]
|
||||
|
||||
async def test_sniff_reuses_connection_instances_if_possible(self):
|
||||
t = AsyncTransport(
|
||||
[{"data": CLUSTER_NODES}, {"host": "1.1.1.1", "port": 123}],
|
||||
connection_class=DummyConnection,
|
||||
randomize_hosts=False,
|
||||
)
|
||||
await t._async_call()
|
||||
connection = t.connection_pool.connections[1]
|
||||
connection.delay = 3.0 # Add this delay to make the sniffing deterministic.
|
||||
|
||||
await t.sniff_hosts()
|
||||
assert 1 == len(t.connection_pool.connections)
|
||||
assert connection is t.get_connection()
|
||||
|
||||
async def test_sniff_on_fail_triggers_sniffing_on_fail(self):
|
||||
t = AsyncTransport(
|
||||
[{"exception": ConnectionError("abandon ship")}, {"data": CLUSTER_NODES}],
|
||||
connection_class=DummyConnection,
|
||||
sniff_on_connection_fail=True,
|
||||
max_retries=0,
|
||||
randomize_hosts=False,
|
||||
)
|
||||
await t._async_call()
|
||||
|
||||
connection_error = False
|
||||
try:
|
||||
await t.perform_request("GET", "/")
|
||||
except ConnectionError:
|
||||
connection_error = True
|
||||
|
||||
await t.sniffing_task # Need to wait for the sniffing task to complete
|
||||
|
||||
assert connection_error
|
||||
assert 1 == len(t.connection_pool.connections)
|
||||
assert "http://1.1.1.1:123" == t.get_connection().host
|
||||
|
||||
@patch("opensearchpy._async.transport.AsyncTransport.sniff_hosts")
|
||||
async def test_sniff_on_fail_failing_does_not_prevent_retires(self, sniff_hosts):
|
||||
sniff_hosts.side_effect = [TransportError("sniff failed")]
|
||||
t = AsyncTransport(
|
||||
[{"exception": ConnectionError("abandon ship")}, {"data": CLUSTER_NODES}],
|
||||
connection_class=DummyConnection,
|
||||
sniff_on_connection_fail=True,
|
||||
max_retries=3,
|
||||
randomize_hosts=False,
|
||||
)
|
||||
await t._async_init()
|
||||
|
||||
conn_err, conn_data = t.connection_pool.connections
|
||||
response = await t.perform_request("GET", "/")
|
||||
assert json.loads(CLUSTER_NODES) == response
|
||||
assert 1 == sniff_hosts.call_count
|
||||
assert 1 == len(conn_err.calls)
|
||||
assert 1 == len(conn_data.calls)
|
||||
|
||||
async def test_sniff_after_n_seconds(self, event_loop):
|
||||
t = AsyncTransport(
|
||||
[{"data": CLUSTER_NODES}],
|
||||
connection_class=DummyConnection,
|
||||
sniffer_timeout=5,
|
||||
)
|
||||
await t._async_call()
|
||||
|
||||
for _ in range(4):
|
||||
await t.perform_request("GET", "/")
|
||||
assert 1 == len(t.connection_pool.connections)
|
||||
assert isinstance(t.get_connection(), DummyConnection)
|
||||
t.last_sniff = event_loop.time() - 5.1
|
||||
|
||||
await t.perform_request("GET", "/")
|
||||
await t.sniffing_task # Need to wait for the sniffing task to complete
|
||||
|
||||
assert 1 == len(t.connection_pool.connections)
|
||||
assert "http://1.1.1.1:123" == t.get_connection().host
|
||||
assert event_loop.time() - 1 < t.last_sniff < event_loop.time() + 0.01
|
||||
|
||||
async def test_sniff_7x_publish_host(self):
|
||||
# Test the response shaped when a 7.x node has publish_host set
|
||||
# and the returend data is shaped in the fqdn/ip:port format.
|
||||
t = AsyncTransport(
|
||||
[{"data": CLUSTER_NODES_7x_PUBLISH_HOST}],
|
||||
connection_class=DummyConnection,
|
||||
sniff_timeout=42,
|
||||
)
|
||||
await t._async_call()
|
||||
await t.sniff_hosts()
|
||||
# Ensure we parsed out the fqdn and port from the fqdn/ip:port string.
|
||||
assert t.connection_pool.connection_opts[0][1] == {
|
||||
"host": "somehost.tld",
|
||||
"port": 123,
|
||||
}
|
||||
|
||||
@patch("opensearchpy._async.transport.AsyncTransport.sniff_hosts")
|
||||
async def test_sniffing_disabled_on_cloud_instances(self, sniff_hosts):
|
||||
t = AsyncTransport(
|
||||
[{}],
|
||||
sniff_on_start=True,
|
||||
sniff_on_connection_fail=True,
|
||||
connection_class=DummyConnection,
|
||||
cloud_id="cluster:dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyQ0ZmE4ODIxZTc1NjM0MDMyYmVkMWNmMjIxMTBlMmY5NyQ0ZmE4ODIxZTc1NjM0MDMyYmVkMWNmMjIxMTBlMmY5Ng==",
|
||||
)
|
||||
await t._async_call()
|
||||
|
||||
assert not t.sniff_on_connection_fail
|
||||
assert sniff_hosts.call_args is None # Assert not called.
|
||||
await t.perform_request("GET", "/", body={})
|
||||
assert 1 == len(t.get_connection().calls)
|
||||
assert ("GET", "/", None, b"{}") == t.get_connection().calls[0][0]
|
||||
|
||||
async def test_transport_close_closes_all_pool_connections(self):
|
||||
t = AsyncTransport([{}], connection_class=DummyConnection)
|
||||
await t._async_call()
|
||||
|
||||
assert not any([conn.closed for conn in t.connection_pool.connections])
|
||||
await t.close()
|
||||
assert all([conn.closed for conn in t.connection_pool.connections])
|
||||
|
||||
t = AsyncTransport([{}, {}], connection_class=DummyConnection)
|
||||
await t._async_call()
|
||||
|
||||
assert not any([conn.closed for conn in t.connection_pool.connections])
|
||||
await t.close()
|
||||
assert all([conn.closed for conn in t.connection_pool.connections])
|
||||
|
||||
async def test_sniff_on_start_error_if_no_sniffed_hosts(self, event_loop):
|
||||
t = AsyncTransport(
|
||||
[
|
||||
{"data": ""},
|
||||
{"data": ""},
|
||||
{"data": ""},
|
||||
],
|
||||
connection_class=DummyConnection,
|
||||
sniff_on_start=True,
|
||||
)
|
||||
|
||||
# If our initial sniffing attempt comes back
|
||||
# empty then we raise an error.
|
||||
with pytest.raises(TransportError) as e:
|
||||
await t._async_call()
|
||||
assert str(e.value) == "TransportError(N/A, 'Unable to sniff hosts.')"
|
||||
|
||||
async def test_sniff_on_start_waits_for_sniff_to_complete(self, event_loop):
|
||||
t = AsyncTransport(
|
||||
[
|
||||
{"delay": 1, "data": ""},
|
||||
{"delay": 1, "data": ""},
|
||||
{"delay": 1, "data": CLUSTER_NODES},
|
||||
],
|
||||
connection_class=DummyConnection,
|
||||
sniff_on_start=True,
|
||||
)
|
||||
|
||||
# Start the timer right before the first task
|
||||
# and have a bunch of tasks come in immediately.
|
||||
tasks = []
|
||||
start_time = event_loop.time()
|
||||
for _ in range(5):
|
||||
tasks.append(event_loop.create_task(t._async_call()))
|
||||
await asyncio.sleep(0) # Yield to the loop
|
||||
|
||||
assert t.sniffing_task is not None
|
||||
|
||||
# Tasks streaming in later.
|
||||
for _ in range(5):
|
||||
tasks.append(event_loop.create_task(t._async_call()))
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# Now that all the API calls have come in we wait for
|
||||
# them all to resolve before
|
||||
await asyncio.gather(*tasks)
|
||||
end_time = event_loop.time()
|
||||
duration = end_time - start_time
|
||||
|
||||
# All the tasks blocked on the sniff of each node
|
||||
# and then resolved immediately after.
|
||||
assert 1 <= duration < 2
|
||||
|
||||
async def test_sniff_on_start_close_unlocks_async_calls(self, event_loop):
|
||||
t = AsyncTransport(
|
||||
[
|
||||
{"delay": 10, "data": CLUSTER_NODES},
|
||||
],
|
||||
connection_class=DummyConnection,
|
||||
sniff_on_start=True,
|
||||
)
|
||||
|
||||
# Start making _async_calls() before we cancel
|
||||
tasks = []
|
||||
start_time = event_loop.time()
|
||||
for _ in range(3):
|
||||
tasks.append(event_loop.create_task(t._async_call()))
|
||||
await asyncio.sleep(0)
|
||||
|
||||
# Close the transport while the sniffing task is active! :(
|
||||
await t.close()
|
||||
|
||||
# Now we start waiting on all those _async_calls()
|
||||
await asyncio.gather(*tasks)
|
||||
end_time = event_loop.time()
|
||||
duration = end_time - start_time
|
||||
|
||||
# A lot quicker than 10 seconds defined in 'delay'
|
||||
assert duration < 1
|
||||
@@ -0,0 +1,78 @@
|
||||
# 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.
|
||||
|
||||
from collections import defaultdict
|
||||
from unittest import SkipTest # noqa: F401
|
||||
from unittest import TestCase
|
||||
|
||||
from opensearchpy import OpenSearch
|
||||
|
||||
|
||||
class DummyTransport(object):
|
||||
def __init__(self, hosts, responses=None, **kwargs):
|
||||
self.hosts = hosts
|
||||
self.responses = responses
|
||||
self.call_count = 0
|
||||
self.calls = defaultdict(list)
|
||||
|
||||
def perform_request(self, method, url, params=None, headers=None, body=None):
|
||||
resp = 200, {}
|
||||
if self.responses:
|
||||
resp = self.responses[self.call_count]
|
||||
self.call_count += 1
|
||||
self.calls[(method, url)].append((params, headers, body))
|
||||
return resp
|
||||
|
||||
|
||||
class OpenSearchTestCase(TestCase):
|
||||
def setUp(self):
|
||||
super(OpenSearchTestCase, self).setUp()
|
||||
self.client = OpenSearch(transport_class=DummyTransport)
|
||||
|
||||
def assert_call_count_equals(self, count):
|
||||
self.assertEqual(count, self.client.transport.call_count)
|
||||
|
||||
def assert_url_called(self, method, url, count=1):
|
||||
self.assertIn((method, url), self.client.transport.calls)
|
||||
calls = self.client.transport.calls[(method, url)]
|
||||
self.assertEqual(count, len(calls))
|
||||
return calls
|
||||
|
||||
|
||||
class TestOpenSearchTestCase(OpenSearchTestCase):
|
||||
def test_our_transport_used(self):
|
||||
self.assertIsInstance(self.client.transport, DummyTransport)
|
||||
|
||||
def test_start_with_0_call(self):
|
||||
self.assert_call_count_equals(0)
|
||||
|
||||
def test_each_call_is_recorded(self):
|
||||
self.client.transport.perform_request("GET", "/")
|
||||
self.client.transport.perform_request("DELETE", "/42", params={}, body="body")
|
||||
self.assert_call_count_equals(2)
|
||||
self.assertEqual(
|
||||
[({}, None, "body")], self.assert_url_called("DELETE", "/42", 1)
|
||||
)
|
||||
@@ -0,0 +1,164 @@
|
||||
# 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.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import warnings
|
||||
|
||||
from opensearchpy.client import OpenSearch, _normalize_hosts
|
||||
|
||||
from ..test_cases import OpenSearchTestCase, TestCase
|
||||
|
||||
|
||||
class TestNormalizeHosts(TestCase):
|
||||
def test_none_uses_defaults(self):
|
||||
self.assertEqual([{}], _normalize_hosts(None))
|
||||
|
||||
def test_strings_are_used_as_hostnames(self):
|
||||
self.assertEqual([{"host": "elastic.co"}], _normalize_hosts(["elastic.co"]))
|
||||
|
||||
def test_strings_are_parsed_for_port_and_user(self):
|
||||
self.assertEqual(
|
||||
[
|
||||
{"host": "elastic.co", "port": 42},
|
||||
{"host": "elastic.co", "http_auth": "user:secre]"},
|
||||
],
|
||||
_normalize_hosts(["elastic.co:42", "user:secre%[email protected]"]),
|
||||
)
|
||||
|
||||
def test_strings_are_parsed_for_scheme(self):
|
||||
self.assertEqual(
|
||||
[
|
||||
{"host": "elastic.co", "port": 42, "use_ssl": True},
|
||||
{
|
||||
"host": "elastic.co",
|
||||
"http_auth": "user:secret",
|
||||
"use_ssl": True,
|
||||
"port": 443,
|
||||
"url_prefix": "/prefix",
|
||||
},
|
||||
],
|
||||
_normalize_hosts(
|
||||
["https://elastic.co:42", "https://user:[email protected]/prefix"]
|
||||
),
|
||||
)
|
||||
|
||||
def test_dicts_are_left_unchanged(self):
|
||||
self.assertEqual(
|
||||
[{"host": "local", "extra": 123}],
|
||||
_normalize_hosts([{"host": "local", "extra": 123}]),
|
||||
)
|
||||
|
||||
def test_single_string_is_wrapped_in_list(self):
|
||||
self.assertEqual([{"host": "elastic.co"}], _normalize_hosts("elastic.co"))
|
||||
|
||||
|
||||
class TestClient(OpenSearchTestCase):
|
||||
def test_request_timeout_is_passed_through_unescaped(self):
|
||||
self.client.ping(request_timeout=0.1)
|
||||
calls = self.assert_url_called("HEAD", "/")
|
||||
self.assertEqual([({"request_timeout": 0.1}, {}, None)], calls)
|
||||
|
||||
def test_params_is_copied_when(self):
|
||||
rt = object()
|
||||
params = dict(request_timeout=rt)
|
||||
self.client.ping(params=params)
|
||||
self.client.ping(params=params)
|
||||
calls = self.assert_url_called("HEAD", "/", 2)
|
||||
self.assertEqual(
|
||||
[({"request_timeout": rt}, {}, None), ({"request_timeout": rt}, {}, None)],
|
||||
calls,
|
||||
)
|
||||
self.assertFalse(calls[0][0] is calls[1][0])
|
||||
|
||||
def test_headers_is_copied_when(self):
|
||||
hv = "value"
|
||||
headers = dict(Authentication=hv)
|
||||
self.client.ping(headers=headers)
|
||||
self.client.ping(headers=headers)
|
||||
calls = self.assert_url_called("HEAD", "/", 2)
|
||||
self.assertEqual(
|
||||
[({}, {"authentication": hv}, None), ({}, {"authentication": hv}, None)],
|
||||
calls,
|
||||
)
|
||||
self.assertFalse(calls[0][0] is calls[1][0])
|
||||
|
||||
def test_from_in_search(self):
|
||||
self.client.search(index="i", from_=10)
|
||||
calls = self.assert_url_called("POST", "/i/_search")
|
||||
self.assertEqual([({"from": "10"}, {}, None)], calls)
|
||||
|
||||
def test_repr_contains_hosts(self):
|
||||
self.assertEqual("<OpenSearch([{}])>", repr(self.client))
|
||||
|
||||
def test_repr_subclass(self):
|
||||
class OtherOpenSearch(OpenSearch):
|
||||
pass
|
||||
|
||||
self.assertEqual("<OtherOpenSearch([{}])>", repr(OtherOpenSearch()))
|
||||
|
||||
def test_repr_contains_hosts_passed_in(self):
|
||||
self.assertIn("opensearchpy.org", repr(OpenSearch(["opensearch.org:123"])))
|
||||
|
||||
def test_repr_truncates_host_to_5(self):
|
||||
hosts = [{"host": "opensearch" + str(i)} for i in range(10)]
|
||||
client = OpenSearch(hosts)
|
||||
self.assertNotIn("opensearch5", repr(client))
|
||||
self.assertIn("...", repr(client))
|
||||
|
||||
def test_index_uses_post_if_id_is_empty(self):
|
||||
self.client.index(index="my-index", id="", body={})
|
||||
|
||||
self.assert_url_called("POST", "/my-index/_doc")
|
||||
|
||||
def test_index_uses_put_if_id_is_not_empty(self):
|
||||
self.client.index(index="my-index", id=0, body={})
|
||||
|
||||
self.assert_url_called("PUT", "/my-index/_doc/0")
|
||||
|
||||
def test_tasks_get_without_task_id_deprecated(self):
|
||||
warnings.simplefilter("always", DeprecationWarning)
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
self.client.tasks.get()
|
||||
|
||||
self.assert_url_called("GET", "/_tasks")
|
||||
self.assertEqual(len(w), 1)
|
||||
self.assertIs(w[0].category, DeprecationWarning)
|
||||
self.assertEqual(
|
||||
str(w[0].message),
|
||||
"Calling client.tasks.get() without a task_id is deprecated "
|
||||
"and will be removed in v8.0. Use client.tasks.list() instead.",
|
||||
)
|
||||
|
||||
def test_tasks_get_with_task_id_not_deprecated(self):
|
||||
warnings.simplefilter("always", DeprecationWarning)
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
self.client.tasks.get("task-1")
|
||||
self.client.tasks.get(task_id="task-2")
|
||||
|
||||
self.assert_url_called("GET", "/_tasks/task-1")
|
||||
self.assert_url_called("GET", "/_tasks/task-2")
|
||||
self.assertEqual(len(w), 0)
|
||||
@@ -0,0 +1,53 @@
|
||||
# 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.
|
||||
|
||||
from test_opensearchpy.test_cases import OpenSearchTestCase
|
||||
|
||||
|
||||
class TestCluster(OpenSearchTestCase):
|
||||
def test_stats_without_node_id(self):
|
||||
self.client.cluster.stats()
|
||||
self.assert_url_called("GET", "/_cluster/stats")
|
||||
|
||||
def test_stats_with_node_id(self):
|
||||
self.client.cluster.stats("node-1")
|
||||
self.assert_url_called("GET", "/_cluster/stats/nodes/node-1")
|
||||
|
||||
self.client.cluster.stats(node_id="node-2")
|
||||
self.assert_url_called("GET", "/_cluster/stats/nodes/node-2")
|
||||
|
||||
def test_state_with_index_without_metric_defaults_to_all(self):
|
||||
self.client.cluster.state()
|
||||
self.assert_url_called("GET", "/_cluster/state")
|
||||
|
||||
self.client.cluster.state(metric="cluster_name")
|
||||
self.assert_url_called("GET", "/_cluster/state/cluster_name")
|
||||
|
||||
self.client.cluster.state(index="index-1")
|
||||
self.assert_url_called("GET", "/_cluster/state/_all/index-1")
|
||||
|
||||
self.client.cluster.state(index="index-1", metric="cluster_name")
|
||||
self.assert_url_called("GET", "/_cluster/state/cluster_name/index-1")
|
||||
@@ -0,0 +1,50 @@
|
||||
# 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.
|
||||
|
||||
from test_opensearchpy.test_cases import OpenSearchTestCase
|
||||
|
||||
|
||||
class TestIndices(OpenSearchTestCase):
|
||||
def test_create_one_index(self):
|
||||
self.client.indices.create("test-index")
|
||||
self.assert_url_called("PUT", "/test-index")
|
||||
|
||||
def test_delete_multiple_indices(self):
|
||||
self.client.indices.delete(["test-index", "second.index", "third/index"])
|
||||
self.assert_url_called("DELETE", "/test-index,second.index,third%2Findex")
|
||||
|
||||
def test_exists_index(self):
|
||||
self.client.indices.exists("second.index,third/index")
|
||||
self.assert_url_called("HEAD", "/second.index,third%2Findex")
|
||||
|
||||
def test_passing_empty_value_for_required_param_raises_exception(self):
|
||||
self.assertRaises(ValueError, self.client.indices.exists, index=None)
|
||||
self.assertRaises(ValueError, self.client.indices.exists, index=[])
|
||||
self.assertRaises(ValueError, self.client.indices.exists, index="")
|
||||
|
||||
def test_put_mapping_without_index(self):
|
||||
self.client.indices.put_mapping(doc_type="doc-type", body={})
|
||||
self.assert_url_called("PUT", "/_all/doc-type/_mapping")
|
||||
@@ -0,0 +1,173 @@
|
||||
# -*- 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 pytest
|
||||
|
||||
from test_opensearchpy.test_cases import OpenSearchTestCase
|
||||
|
||||
|
||||
class TestOverriddenUrlTargets(OpenSearchTestCase):
|
||||
def test_create(self):
|
||||
self.client.create(index="test-index", id="test-id", body={})
|
||||
self.assert_url_called("PUT", "/test-index/_create/test-id")
|
||||
|
||||
self.client.create(
|
||||
index="test-index", doc_type="test-type", id="test-id", body={}
|
||||
)
|
||||
self.assert_url_called("PUT", "/test-index/test-type/test-id/_create")
|
||||
|
||||
def test_delete(self):
|
||||
self.client.delete(index="test-index", id="test-id")
|
||||
self.assert_url_called("DELETE", "/test-index/_doc/test-id")
|
||||
|
||||
self.client.delete(index="test-index", doc_type="test-type", id="test-id")
|
||||
self.assert_url_called("DELETE", "/test-index/test-type/test-id")
|
||||
|
||||
def test_exists(self):
|
||||
self.client.exists(index="test-index", id="test-id")
|
||||
self.assert_url_called("HEAD", "/test-index/_doc/test-id")
|
||||
|
||||
self.client.exists(index="test-index", doc_type="test-type", id="test-id")
|
||||
self.assert_url_called("HEAD", "/test-index/test-type/test-id")
|
||||
|
||||
def test_explain(self):
|
||||
self.client.explain(index="test-index", id="test-id")
|
||||
self.assert_url_called("POST", "/test-index/_explain/test-id")
|
||||
|
||||
self.client.explain(index="test-index", doc_type="test-type", id="test-id")
|
||||
self.assert_url_called("POST", "/test-index/test-type/test-id/_explain")
|
||||
|
||||
def test_get(self):
|
||||
self.client.get(index="test-index", id="test-id")
|
||||
self.assert_url_called("GET", "/test-index/_doc/test-id")
|
||||
|
||||
self.client.get(index="test-index", doc_type="test-type", id="test-id")
|
||||
self.assert_url_called("GET", "/test-index/test-type/test-id")
|
||||
|
||||
def test_get_source(self):
|
||||
self.client.get_source(index="test-index", id="test-id")
|
||||
self.assert_url_called("GET", "/test-index/_source/test-id")
|
||||
|
||||
self.client.get_source(index="test-index", doc_type="test-type", id="test-id")
|
||||
self.assert_url_called("GET", "/test-index/test-type/test-id/_source")
|
||||
|
||||
def test_exists_source(self):
|
||||
self.client.exists_source(index="test-index", id="test-id")
|
||||
self.assert_url_called("HEAD", "/test-index/_source/test-id")
|
||||
|
||||
self.client.exists_source(
|
||||
index="test-index", doc_type="test-type", id="test-id"
|
||||
)
|
||||
self.assert_url_called("HEAD", "/test-index/test-type/test-id/_source")
|
||||
|
||||
def test_index(self):
|
||||
self.client.index(index="test-index", body={})
|
||||
self.assert_url_called("POST", "/test-index/_doc")
|
||||
|
||||
self.client.index(index="test-index", id="test-id", body={})
|
||||
self.assert_url_called("PUT", "/test-index/_doc/test-id")
|
||||
|
||||
self.client.index(index="test-index", doc_type="test-type", body={})
|
||||
self.assert_url_called("POST", "/test-index/test-type")
|
||||
|
||||
self.client.index(
|
||||
index="test-index", doc_type="test-type", id="test-id", body={}
|
||||
)
|
||||
self.assert_url_called("PUT", "/test-index/test-type/test-id")
|
||||
|
||||
self.client.index(index="test-index", doc_type="_doc", body={})
|
||||
self.assert_url_called("POST", "/test-index/_doc", count=2)
|
||||
|
||||
self.client.index(index="test-index", doc_type="_doc", id="test-id", body={})
|
||||
self.assert_url_called("PUT", "/test-index/_doc/test-id", count=2)
|
||||
|
||||
def test_termvectors(self):
|
||||
self.client.termvectors(index="test-index", body={})
|
||||
self.assert_url_called("POST", "/test-index/_termvectors")
|
||||
|
||||
self.client.termvectors(index="test-index", id="test-id", body={})
|
||||
self.assert_url_called("POST", "/test-index/_termvectors/test-id")
|
||||
|
||||
self.client.termvectors(index="test-index", doc_type="test-type", body={})
|
||||
self.assert_url_called("POST", "/test-index/test-type/_termvectors")
|
||||
|
||||
self.client.termvectors(
|
||||
index="test-index", doc_type="test-type", id="test-id", body={}
|
||||
)
|
||||
self.assert_url_called("POST", "/test-index/test-type/test-id/_termvectors")
|
||||
|
||||
def test_mtermvectors(self):
|
||||
self.client.mtermvectors(index="test-index", body={})
|
||||
self.assert_url_called("POST", "/test-index/_mtermvectors")
|
||||
|
||||
self.client.mtermvectors(index="test-index", doc_type="test-type", body={})
|
||||
self.assert_url_called("POST", "/test-index/test-type/_mtermvectors")
|
||||
|
||||
def test_update(self):
|
||||
self.client.update(index="test-index", id="test-id", body={})
|
||||
self.assert_url_called("POST", "/test-index/_update/test-id")
|
||||
|
||||
self.client.update(
|
||||
index="test-index", doc_type="test-type", id="test-id", body={}
|
||||
)
|
||||
self.assert_url_called("POST", "/test-index/test-type/test-id/_update")
|
||||
|
||||
def test_cluster_state(self):
|
||||
self.client.cluster.state()
|
||||
self.assert_url_called("GET", "/_cluster/state")
|
||||
|
||||
self.client.cluster.state(index="test-index")
|
||||
self.assert_url_called("GET", "/_cluster/state/_all/test-index")
|
||||
|
||||
self.client.cluster.state(index="test-index", metric="test-metric")
|
||||
self.assert_url_called("GET", "/_cluster/state/test-metric/test-index")
|
||||
|
||||
def test_cluster_stats(self):
|
||||
self.client.cluster.stats()
|
||||
self.assert_url_called("GET", "/_cluster/stats")
|
||||
|
||||
self.client.cluster.stats(node_id="test-node")
|
||||
self.assert_url_called("GET", "/_cluster/stats/nodes/test-node")
|
||||
|
||||
def test_indices_put_mapping(self):
|
||||
self.client.indices.put_mapping(body={})
|
||||
self.assert_url_called("PUT", "/_mapping")
|
||||
|
||||
self.client.indices.put_mapping(index="test-index", body={})
|
||||
self.assert_url_called("PUT", "/test-index/_mapping")
|
||||
|
||||
self.client.indices.put_mapping(
|
||||
index="test-index", doc_type="test-type", body={}
|
||||
)
|
||||
self.assert_url_called("PUT", "/test-index/test-type/_mapping")
|
||||
|
||||
self.client.indices.put_mapping(doc_type="test-type", body={})
|
||||
self.assert_url_called("PUT", "/_all/test-type/_mapping")
|
||||
|
||||
def test_tasks_get(self):
|
||||
with pytest.warns(DeprecationWarning):
|
||||
self.client.tasks.get()
|
||||
@@ -0,0 +1,182 @@
|
||||
# -*- 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.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from opensearchpy.client.utils import _bulk_body, _escape, _make_path, query_params
|
||||
from opensearchpy.compat import PY2
|
||||
|
||||
from ..test_cases import SkipTest, TestCase
|
||||
|
||||
|
||||
class TestQueryParams(TestCase):
|
||||
def setup_method(self, _):
|
||||
self.calls = []
|
||||
|
||||
@query_params("simple_param")
|
||||
def func_to_wrap(self, *args, **kwargs):
|
||||
self.calls.append((args, kwargs))
|
||||
|
||||
def test_handles_params(self):
|
||||
self.func_to_wrap(params={"simple_param_2": "2"}, simple_param="3")
|
||||
self.assertEqual(
|
||||
self.calls,
|
||||
[
|
||||
(
|
||||
(),
|
||||
{
|
||||
"params": {"simple_param": b"3", "simple_param_2": "2"},
|
||||
"headers": {},
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
def test_handles_headers(self):
|
||||
self.func_to_wrap(headers={"X-Opaque-Id": "app-1"})
|
||||
self.assertEqual(
|
||||
self.calls, [((), {"params": {}, "headers": {"x-opaque-id": "app-1"}})]
|
||||
)
|
||||
|
||||
def test_handles_opaque_id(self):
|
||||
self.func_to_wrap(opaque_id="request-id")
|
||||
self.assertEqual(
|
||||
self.calls, [((), {"params": {}, "headers": {"x-opaque-id": "request-id"}})]
|
||||
)
|
||||
|
||||
def test_handles_empty_none_and_normalization(self):
|
||||
self.func_to_wrap(params=None)
|
||||
self.assertEqual(self.calls[-1], ((), {"params": {}, "headers": {}}))
|
||||
|
||||
self.func_to_wrap(headers=None)
|
||||
self.assertEqual(self.calls[-1], ((), {"params": {}, "headers": {}}))
|
||||
|
||||
self.func_to_wrap(headers=None, params=None)
|
||||
self.assertEqual(self.calls[-1], ((), {"params": {}, "headers": {}}))
|
||||
|
||||
self.func_to_wrap(headers={}, params={})
|
||||
self.assertEqual(self.calls[-1], ((), {"params": {}, "headers": {}}))
|
||||
|
||||
self.func_to_wrap(headers={"X": "y"})
|
||||
self.assertEqual(self.calls[-1], ((), {"params": {}, "headers": {"x": "y"}}))
|
||||
|
||||
def test_per_call_authentication(self):
|
||||
self.func_to_wrap(api_key=("name", "key"))
|
||||
self.assertEqual(
|
||||
self.calls[-1],
|
||||
((), {"headers": {"authorization": "ApiKey bmFtZTprZXk="}, "params": {}}),
|
||||
)
|
||||
|
||||
self.func_to_wrap(http_auth=("user", "password"))
|
||||
self.assertEqual(
|
||||
self.calls[-1],
|
||||
(
|
||||
(),
|
||||
{
|
||||
"headers": {"authorization": "Basic dXNlcjpwYXNzd29yZA=="},
|
||||
"params": {},
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
self.func_to_wrap(http_auth="abcdef")
|
||||
self.assertEqual(
|
||||
self.calls[-1],
|
||||
((), {"headers": {"authorization": "Basic abcdef"}, "params": {}}),
|
||||
)
|
||||
|
||||
# If one or the other is 'None' it's all good!
|
||||
self.func_to_wrap(http_auth=None, api_key=None)
|
||||
self.assertEqual(self.calls[-1], ((), {"headers": {}, "params": {}}))
|
||||
|
||||
self.func_to_wrap(http_auth="abcdef", api_key=None)
|
||||
self.assertEqual(
|
||||
self.calls[-1],
|
||||
((), {"headers": {"authorization": "Basic abcdef"}, "params": {}}),
|
||||
)
|
||||
|
||||
# If both are given values an error is raised.
|
||||
with self.assertRaises(ValueError) as e:
|
||||
self.func_to_wrap(http_auth="key", api_key=("1", "2"))
|
||||
self.assertEqual(
|
||||
str(e.exception),
|
||||
"Only one of 'http_auth' and 'api_key' may be passed at a time",
|
||||
)
|
||||
|
||||
|
||||
class TestMakePath(TestCase):
|
||||
def test_handles_unicode(self):
|
||||
id = "中文"
|
||||
self.assertEqual(
|
||||
"/some-index/type/%E4%B8%AD%E6%96%87", _make_path("some-index", "type", id)
|
||||
)
|
||||
|
||||
def test_handles_utf_encoded_string(self):
|
||||
if not PY2:
|
||||
raise SkipTest("Only relevant for py2")
|
||||
id = "中文".encode("utf-8")
|
||||
self.assertEqual(
|
||||
"/some-index/type/%E4%B8%AD%E6%96%87", _make_path("some-index", "type", id)
|
||||
)
|
||||
|
||||
|
||||
class TestEscape(TestCase):
|
||||
def test_handles_ascii(self):
|
||||
string = "abc123"
|
||||
self.assertEqual(b"abc123", _escape(string))
|
||||
|
||||
def test_handles_unicode(self):
|
||||
string = "中文"
|
||||
self.assertEqual(b"\xe4\xb8\xad\xe6\x96\x87", _escape(string))
|
||||
|
||||
def test_handles_bytestring(self):
|
||||
string = b"celery-task-meta-c4f1201f-eb7b-41d5-9318-a75a8cfbdaa0"
|
||||
self.assertEqual(string, _escape(string))
|
||||
|
||||
|
||||
class TestBulkBody(TestCase):
|
||||
def test_proper_bulk_body_as_string_is_not_modified(self):
|
||||
string_body = '"{"index":{ "_index" : "test"}}\n{"field1": "value1"}"\n'
|
||||
self.assertEqual(string_body, _bulk_body(None, string_body))
|
||||
|
||||
def test_proper_bulk_body_as_bytestring_is_not_modified(self):
|
||||
bytestring_body = b'"{"index":{ "_index" : "test"}}\n{"field1": "value1"}"\n'
|
||||
self.assertEqual(bytestring_body, _bulk_body(None, bytestring_body))
|
||||
|
||||
def test_bulk_body_as_string_adds_trailing_newline(self):
|
||||
string_body = '"{"index":{ "_index" : "test"}}\n{"field1": "value1"}"'
|
||||
self.assertEqual(
|
||||
'"{"index":{ "_index" : "test"}}\n{"field1": "value1"}"\n',
|
||||
_bulk_body(None, string_body),
|
||||
)
|
||||
|
||||
def test_bulk_body_as_bytestring_adds_trailing_newline(self):
|
||||
bytestring_body = b'"{"index":{ "_index" : "test"}}\n{"field1": "value1"}"'
|
||||
self.assertEqual(
|
||||
b'"{"index":{ "_index" : "test"}}\n{"field1": "value1"}"\n',
|
||||
_bulk_body(None, bytestring_body),
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,166 @@
|
||||
# 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 time
|
||||
|
||||
from opensearchpy.connection import Connection
|
||||
from opensearchpy.connection_pool import (
|
||||
ConnectionPool,
|
||||
DummyConnectionPool,
|
||||
RoundRobinSelector,
|
||||
)
|
||||
from opensearchpy.exceptions import ImproperlyConfigured
|
||||
|
||||
from .test_cases import TestCase
|
||||
|
||||
|
||||
class TestConnectionPool(TestCase):
|
||||
def test_dummy_cp_raises_exception_on_more_connections(self):
|
||||
self.assertRaises(ImproperlyConfigured, DummyConnectionPool, [])
|
||||
self.assertRaises(
|
||||
ImproperlyConfigured, DummyConnectionPool, [object(), object()]
|
||||
)
|
||||
|
||||
def test_raises_exception_when_no_connections_defined(self):
|
||||
self.assertRaises(ImproperlyConfigured, ConnectionPool, [])
|
||||
|
||||
def test_default_round_robin(self):
|
||||
pool = ConnectionPool([(x, {}) for x in range(100)])
|
||||
|
||||
connections = set()
|
||||
for _ in range(100):
|
||||
connections.add(pool.get_connection())
|
||||
self.assertEqual(connections, set(range(100)))
|
||||
|
||||
def test_disable_shuffling(self):
|
||||
pool = ConnectionPool([(x, {}) for x in range(100)], randomize_hosts=False)
|
||||
|
||||
connections = []
|
||||
for _ in range(100):
|
||||
connections.append(pool.get_connection())
|
||||
self.assertEqual(connections, list(range(100)))
|
||||
|
||||
def test_selectors_have_access_to_connection_opts(self):
|
||||
class MySelector(RoundRobinSelector):
|
||||
def select(self, connections):
|
||||
return self.connection_opts[
|
||||
super(MySelector, self).select(connections)
|
||||
]["actual"]
|
||||
|
||||
pool = ConnectionPool(
|
||||
[(x, {"actual": x * x}) for x in range(100)],
|
||||
selector_class=MySelector,
|
||||
randomize_hosts=False,
|
||||
)
|
||||
|
||||
connections = []
|
||||
for _ in range(100):
|
||||
connections.append(pool.get_connection())
|
||||
self.assertEqual(connections, [x * x for x in range(100)])
|
||||
|
||||
def test_dead_nodes_are_removed_from_active_connections(self):
|
||||
pool = ConnectionPool([(x, {}) for x in range(100)])
|
||||
|
||||
now = time.time()
|
||||
pool.mark_dead(42, now=now)
|
||||
self.assertEqual(99, len(pool.connections))
|
||||
self.assertEqual(1, pool.dead.qsize())
|
||||
self.assertEqual((now + 60, 42), pool.dead.get())
|
||||
|
||||
def test_connection_is_skipped_when_dead(self):
|
||||
pool = ConnectionPool([(x, {}) for x in range(2)])
|
||||
pool.mark_dead(0)
|
||||
|
||||
self.assertEqual(
|
||||
[1, 1, 1],
|
||||
[pool.get_connection(), pool.get_connection(), pool.get_connection()],
|
||||
)
|
||||
|
||||
def test_new_connection_is_not_marked_dead(self):
|
||||
# Create 10 connections
|
||||
pool = ConnectionPool([(Connection(), {}) for _ in range(10)])
|
||||
|
||||
# Pass in a new connection that is not in the pool to mark as dead
|
||||
new_connection = Connection()
|
||||
pool.mark_dead(new_connection)
|
||||
|
||||
# Nothing should be marked dead
|
||||
self.assertEqual(0, len(pool.dead_count))
|
||||
|
||||
def test_connection_is_forcibly_resurrected_when_no_live_ones_are_availible(self):
|
||||
pool = ConnectionPool([(x, {}) for x in range(2)])
|
||||
pool.dead_count[0] = 1
|
||||
pool.mark_dead(0) # failed twice, longer timeout
|
||||
pool.mark_dead(1) # failed the first time, first to be resurrected
|
||||
|
||||
self.assertEqual([], pool.connections)
|
||||
self.assertEqual(1, pool.get_connection())
|
||||
self.assertEqual([1], pool.connections)
|
||||
|
||||
def test_connection_is_resurrected_after_its_timeout(self):
|
||||
pool = ConnectionPool([(x, {}) for x in range(100)])
|
||||
|
||||
now = time.time()
|
||||
pool.mark_dead(42, now=now - 61)
|
||||
pool.get_connection()
|
||||
self.assertEqual(42, pool.connections[-1])
|
||||
self.assertEqual(100, len(pool.connections))
|
||||
|
||||
def test_force_resurrect_always_returns_a_connection(self):
|
||||
pool = ConnectionPool([(0, {})])
|
||||
|
||||
pool.connections = []
|
||||
self.assertEqual(0, pool.get_connection())
|
||||
self.assertEqual([], pool.connections)
|
||||
self.assertTrue(pool.dead.empty())
|
||||
|
||||
def test_already_failed_connection_has_longer_timeout(self):
|
||||
pool = ConnectionPool([(x, {}) for x in range(100)])
|
||||
now = time.time()
|
||||
pool.dead_count[42] = 2
|
||||
pool.mark_dead(42, now=now)
|
||||
|
||||
self.assertEqual(3, pool.dead_count[42])
|
||||
self.assertEqual((now + 4 * 60, 42), pool.dead.get())
|
||||
|
||||
def test_timeout_for_failed_connections_is_limitted(self):
|
||||
pool = ConnectionPool([(x, {}) for x in range(100)])
|
||||
now = time.time()
|
||||
pool.dead_count[42] = 245
|
||||
pool.mark_dead(42, now=now)
|
||||
|
||||
self.assertEqual(246, pool.dead_count[42])
|
||||
self.assertEqual((now + 32 * 60, 42), pool.dead.get())
|
||||
|
||||
def test_dead_count_is_wiped_clean_for_connection_if_marked_live(self):
|
||||
pool = ConnectionPool([(x, {}) for x in range(100)])
|
||||
now = time.time()
|
||||
pool.dead_count[42] = 2
|
||||
pool.mark_dead(42, now=now)
|
||||
|
||||
self.assertEqual(3, pool.dead_count[42])
|
||||
pool.mark_live(42)
|
||||
self.assertNotIn(42, pool.dead_count)
|
||||
@@ -0,0 +1,52 @@
|
||||
# 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.
|
||||
|
||||
from opensearchpy.exceptions import TransportError
|
||||
|
||||
from .test_cases import TestCase
|
||||
|
||||
|
||||
class TestTransformError(TestCase):
|
||||
def test_transform_error_parse_with_error_reason(self):
|
||||
e = TransportError(
|
||||
500,
|
||||
"InternalServerError",
|
||||
{"error": {"root_cause": [{"type": "error", "reason": "error reason"}]}},
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
str(e), "TransportError(500, 'InternalServerError', 'error reason')"
|
||||
)
|
||||
|
||||
def test_transform_error_parse_with_error_string(self):
|
||||
e = TransportError(
|
||||
500, "InternalServerError", {"error": "something error message"}
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
str(e),
|
||||
"TransportError(500, 'InternalServerError', 'something error message')",
|
||||
)
|
||||
@@ -0,0 +1,221 @@
|
||||
# -*- 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 threading
|
||||
import time
|
||||
|
||||
import mock
|
||||
import pytest
|
||||
|
||||
from opensearchpy import OpenSearch, helpers
|
||||
from opensearchpy.serializer import JSONSerializer
|
||||
|
||||
from .test_cases import TestCase
|
||||
|
||||
lock_side_effect = threading.Lock()
|
||||
|
||||
|
||||
def mock_process_bulk_chunk(*args, **kwargs):
|
||||
"""
|
||||
Threadsafe way of mocking process bulk chunk:
|
||||
https://stackoverflow.com/questions/39332139/thread-safe-version-of-mock-call-count
|
||||
"""
|
||||
|
||||
with lock_side_effect:
|
||||
mock_process_bulk_chunk.call_count += 1
|
||||
time.sleep(0.1)
|
||||
return []
|
||||
|
||||
|
||||
mock_process_bulk_chunk.call_count = 0
|
||||
|
||||
|
||||
class TestParallelBulk(TestCase):
|
||||
@mock.patch(
|
||||
"opensearchpy.helpers.actions._process_bulk_chunk",
|
||||
side_effect=mock_process_bulk_chunk,
|
||||
)
|
||||
def test_all_chunks_sent(self, _process_bulk_chunk):
|
||||
actions = ({"x": i} for i in range(100))
|
||||
list(helpers.parallel_bulk(OpenSearch(), actions, chunk_size=2))
|
||||
|
||||
self.assertEqual(50, mock_process_bulk_chunk.call_count)
|
||||
|
||||
@pytest.mark.skip
|
||||
@mock.patch(
|
||||
"opensearchpy.helpers.actions._process_bulk_chunk",
|
||||
# make sure we spend some time in the thread
|
||||
side_effect=lambda *a: [
|
||||
(True, time.sleep(0.001) or threading.current_thread().ident)
|
||||
],
|
||||
)
|
||||
def test_chunk_sent_from_different_threads(self, _process_bulk_chunk):
|
||||
actions = ({"x": i} for i in range(100))
|
||||
results = list(
|
||||
helpers.parallel_bulk(OpenSearch(), actions, thread_count=10, chunk_size=2)
|
||||
)
|
||||
self.assertTrue(len(set([r[1] for r in results])) > 1)
|
||||
|
||||
|
||||
class TestChunkActions(TestCase):
|
||||
def setup_method(self, _):
|
||||
self.actions = [({"index": {}}, {"some": u"datá", "i": i}) for i in range(100)]
|
||||
|
||||
def test_expand_action(self):
|
||||
self.assertEqual(helpers.expand_action({}), ({"index": {}}, {}))
|
||||
self.assertEqual(
|
||||
helpers.expand_action({"key": "val"}), ({"index": {}}, {"key": "val"})
|
||||
)
|
||||
|
||||
def test_expand_action_actions(self):
|
||||
self.assertEqual(
|
||||
helpers.expand_action(
|
||||
{"_op_type": "delete", "_id": "id", "_index": "index"}
|
||||
),
|
||||
({"delete": {"_id": "id", "_index": "index"}}, None),
|
||||
)
|
||||
self.assertEqual(
|
||||
helpers.expand_action(
|
||||
{"_op_type": "update", "_id": "id", "_index": "index", "key": "val"}
|
||||
),
|
||||
({"update": {"_id": "id", "_index": "index"}}, {"key": "val"}),
|
||||
)
|
||||
self.assertEqual(
|
||||
helpers.expand_action(
|
||||
{"_op_type": "create", "_id": "id", "_index": "index", "key": "val"}
|
||||
),
|
||||
({"create": {"_id": "id", "_index": "index"}}, {"key": "val"}),
|
||||
)
|
||||
self.assertEqual(
|
||||
helpers.expand_action(
|
||||
{
|
||||
"_op_type": "create",
|
||||
"_id": "id",
|
||||
"_index": "index",
|
||||
"_source": {"key": "val"},
|
||||
}
|
||||
),
|
||||
({"create": {"_id": "id", "_index": "index"}}, {"key": "val"}),
|
||||
)
|
||||
|
||||
def test_expand_action_options(self):
|
||||
for option in (
|
||||
"_id",
|
||||
"_index",
|
||||
"_percolate",
|
||||
"_timestamp",
|
||||
"_type",
|
||||
"if_seq_no",
|
||||
"if_primary_term",
|
||||
"parent",
|
||||
"pipeline",
|
||||
"retry_on_conflict",
|
||||
"routing",
|
||||
"version",
|
||||
"version_type",
|
||||
("_parent", "parent"),
|
||||
("_retry_on_conflict", "retry_on_conflict"),
|
||||
("_routing", "routing"),
|
||||
("_version", "version"),
|
||||
("_version_type", "version_type"),
|
||||
("_if_seq_no", "if_seq_no"),
|
||||
("_if_primary_term", "if_primary_term"),
|
||||
):
|
||||
if isinstance(option, str):
|
||||
action_option = option
|
||||
else:
|
||||
option, action_option = option
|
||||
self.assertEqual(
|
||||
helpers.expand_action({"key": "val", option: 0}),
|
||||
({"index": {action_option: 0}}, {"key": "val"}),
|
||||
)
|
||||
|
||||
def test__source_metadata_or_source(self):
|
||||
self.assertEqual(
|
||||
helpers.expand_action({"_source": {"key": "val"}}),
|
||||
({"index": {}}, {"key": "val"}),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
helpers.expand_action(
|
||||
{"_source": ["key"], "key": "val", "_op_type": "update"}
|
||||
),
|
||||
({"update": {"_source": ["key"]}}, {"key": "val"}),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
helpers.expand_action(
|
||||
{"_source": True, "key": "val", "_op_type": "update"}
|
||||
),
|
||||
({"update": {"_source": True}}, {"key": "val"}),
|
||||
)
|
||||
|
||||
# This case is only to ensure backwards compatibility with old functionality.
|
||||
self.assertEqual(
|
||||
helpers.expand_action(
|
||||
{"_source": {"key2": "val2"}, "key": "val", "_op_type": "update"}
|
||||
),
|
||||
({"update": {}}, {"key2": "val2"}),
|
||||
)
|
||||
|
||||
def test_chunks_are_chopped_by_byte_size(self):
|
||||
self.assertEqual(
|
||||
100,
|
||||
len(
|
||||
list(helpers._chunk_actions(self.actions, 100000, 1, JSONSerializer()))
|
||||
),
|
||||
)
|
||||
|
||||
def test_chunks_are_chopped_by_chunk_size(self):
|
||||
self.assertEqual(
|
||||
10,
|
||||
len(
|
||||
list(
|
||||
helpers._chunk_actions(self.actions, 10, 99999999, JSONSerializer())
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
def test_chunks_are_chopped_by_byte_size_properly(self):
|
||||
max_byte_size = 170
|
||||
chunks = list(
|
||||
helpers._chunk_actions(
|
||||
self.actions, 100000, max_byte_size, JSONSerializer()
|
||||
)
|
||||
)
|
||||
self.assertEqual(25, len(chunks))
|
||||
for chunk_data, chunk_actions in chunks:
|
||||
chunk = u"".join(chunk_actions)
|
||||
chunk = chunk if isinstance(chunk, str) else chunk.encode("utf-8")
|
||||
self.assertLessEqual(len(chunk), max_byte_size)
|
||||
|
||||
|
||||
class TestExpandActions(TestCase):
|
||||
def test_string_actions_are_marked_as_simple_inserts(self):
|
||||
self.assertEqual(
|
||||
('{"index":{}}', "whatever"), helpers.expand_action("whatever")
|
||||
)
|
||||
@@ -0,0 +1,235 @@
|
||||
# -*- 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 sys
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
try:
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
except ImportError:
|
||||
np = pd = None
|
||||
|
||||
from opensearchpy.exceptions import ImproperlyConfigured, SerializationError
|
||||
from opensearchpy.serializer import (
|
||||
DEFAULT_SERIALIZERS,
|
||||
Deserializer,
|
||||
JSONSerializer,
|
||||
TextSerializer,
|
||||
)
|
||||
|
||||
from .test_cases import SkipTest, TestCase
|
||||
|
||||
|
||||
def requires_numpy_and_pandas():
|
||||
if np is None or pd is None:
|
||||
raise SkipTest("Test requires numpy or pandas to be available")
|
||||
|
||||
|
||||
class TestJSONSerializer(TestCase):
|
||||
def test_datetime_serialization(self):
|
||||
self.assertEqual(
|
||||
'{"d":"2010-10-01T02:30:00"}',
|
||||
JSONSerializer().dumps({"d": datetime(2010, 10, 1, 2, 30)}),
|
||||
)
|
||||
|
||||
def test_decimal_serialization(self):
|
||||
requires_numpy_and_pandas()
|
||||
|
||||
if sys.version_info[:2] == (2, 6):
|
||||
raise SkipTest("Float rounding is broken in 2.6.")
|
||||
self.assertEqual('{"d":3.8}', JSONSerializer().dumps({"d": Decimal("3.8")}))
|
||||
|
||||
def test_uuid_serialization(self):
|
||||
self.assertEqual(
|
||||
'{"d":"00000000-0000-0000-0000-000000000003"}',
|
||||
JSONSerializer().dumps(
|
||||
{"d": uuid.UUID("00000000-0000-0000-0000-000000000003")}
|
||||
),
|
||||
)
|
||||
|
||||
def test_serializes_numpy_bool(self):
|
||||
requires_numpy_and_pandas()
|
||||
|
||||
self.assertEqual('{"d":true}', JSONSerializer().dumps({"d": np.bool_(True)}))
|
||||
|
||||
def test_serializes_numpy_integers(self):
|
||||
requires_numpy_and_pandas()
|
||||
|
||||
ser = JSONSerializer()
|
||||
for np_type in (
|
||||
np.int_,
|
||||
np.int8,
|
||||
np.int16,
|
||||
np.int32,
|
||||
np.int64,
|
||||
):
|
||||
self.assertEqual(ser.dumps({"d": np_type(-1)}), '{"d":-1}')
|
||||
|
||||
for np_type in (
|
||||
np.uint8,
|
||||
np.uint16,
|
||||
np.uint32,
|
||||
np.uint64,
|
||||
):
|
||||
self.assertEqual(ser.dumps({"d": np_type(1)}), '{"d":1}')
|
||||
|
||||
def test_serializes_numpy_floats(self):
|
||||
requires_numpy_and_pandas()
|
||||
|
||||
ser = JSONSerializer()
|
||||
for np_type in (
|
||||
np.float_,
|
||||
np.float32,
|
||||
np.float64,
|
||||
):
|
||||
self.assertRegexpMatches(
|
||||
ser.dumps({"d": np_type(1.2)}), r'^\{"d":1\.2[\d]*}$'
|
||||
)
|
||||
|
||||
def test_serializes_numpy_datetime(self):
|
||||
requires_numpy_and_pandas()
|
||||
|
||||
self.assertEqual(
|
||||
'{"d":"2010-10-01T02:30:00"}',
|
||||
JSONSerializer().dumps({"d": np.datetime64("2010-10-01T02:30:00")}),
|
||||
)
|
||||
|
||||
def test_serializes_numpy_ndarray(self):
|
||||
requires_numpy_and_pandas()
|
||||
|
||||
self.assertEqual(
|
||||
'{"d":[0,0,0,0,0]}',
|
||||
JSONSerializer().dumps({"d": np.zeros((5,), dtype=np.uint8)}),
|
||||
)
|
||||
# This isn't useful for OpenSearch, just want to make sure it works.
|
||||
self.assertEqual(
|
||||
'{"d":[[0,0],[0,0]]}',
|
||||
JSONSerializer().dumps({"d": np.zeros((2, 2), dtype=np.uint8)}),
|
||||
)
|
||||
|
||||
def test_serializes_numpy_nan_to_nan(self):
|
||||
requires_numpy_and_pandas()
|
||||
|
||||
self.assertEqual(
|
||||
'{"d":NaN}',
|
||||
JSONSerializer().dumps({"d": np.nan}),
|
||||
)
|
||||
|
||||
def test_serializes_pandas_timestamp(self):
|
||||
requires_numpy_and_pandas()
|
||||
|
||||
self.assertEqual(
|
||||
'{"d":"2010-10-01T02:30:00"}',
|
||||
JSONSerializer().dumps({"d": pd.Timestamp("2010-10-01T02:30:00")}),
|
||||
)
|
||||
|
||||
def test_serializes_pandas_series(self):
|
||||
requires_numpy_and_pandas()
|
||||
|
||||
self.assertEqual(
|
||||
'{"d":["a","b","c","d"]}',
|
||||
JSONSerializer().dumps({"d": pd.Series(["a", "b", "c", "d"])}),
|
||||
)
|
||||
|
||||
def test_serializes_pandas_na(self):
|
||||
requires_numpy_and_pandas()
|
||||
|
||||
if not hasattr(pd, "NA"): # pandas.NA added in v1
|
||||
raise SkipTest("pandas.NA required")
|
||||
self.assertEqual(
|
||||
'{"d":null}',
|
||||
JSONSerializer().dumps({"d": pd.NA}),
|
||||
)
|
||||
|
||||
def test_raises_serialization_error_pandas_nat(self):
|
||||
requires_numpy_and_pandas()
|
||||
|
||||
if not hasattr(pd, "NaT"):
|
||||
raise SkipTest("pandas.NaT required")
|
||||
self.assertRaises(SerializationError, JSONSerializer().dumps, {"d": pd.NaT})
|
||||
|
||||
def test_serializes_pandas_category(self):
|
||||
requires_numpy_and_pandas()
|
||||
|
||||
cat = pd.Categorical(["a", "c", "b", "a"], categories=["a", "b", "c"])
|
||||
self.assertEqual(
|
||||
'{"d":["a","c","b","a"]}',
|
||||
JSONSerializer().dumps({"d": cat}),
|
||||
)
|
||||
|
||||
cat = pd.Categorical([1, 2, 3], categories=[1, 2, 3])
|
||||
self.assertEqual(
|
||||
'{"d":[1,2,3]}',
|
||||
JSONSerializer().dumps({"d": cat}),
|
||||
)
|
||||
|
||||
def test_raises_serialization_error_on_dump_error(self):
|
||||
self.assertRaises(SerializationError, JSONSerializer().dumps, object())
|
||||
|
||||
def test_raises_serialization_error_on_load_error(self):
|
||||
self.assertRaises(SerializationError, JSONSerializer().loads, object())
|
||||
self.assertRaises(SerializationError, JSONSerializer().loads, "")
|
||||
self.assertRaises(SerializationError, JSONSerializer().loads, "{{")
|
||||
|
||||
def test_strings_are_left_untouched(self):
|
||||
self.assertEqual("你好", JSONSerializer().dumps("你好"))
|
||||
|
||||
|
||||
class TestTextSerializer(TestCase):
|
||||
def test_strings_are_left_untouched(self):
|
||||
self.assertEqual("你好", TextSerializer().dumps("你好"))
|
||||
|
||||
def test_raises_serialization_error_on_dump_error(self):
|
||||
self.assertRaises(SerializationError, TextSerializer().dumps, {})
|
||||
|
||||
|
||||
class TestDeserializer(TestCase):
|
||||
def setup_method(self, _):
|
||||
self.de = Deserializer(DEFAULT_SERIALIZERS)
|
||||
|
||||
def test_deserializes_json_by_default(self):
|
||||
self.assertEqual({"some": "data"}, self.de.loads('{"some":"data"}'))
|
||||
|
||||
def test_deserializes_text_with_correct_ct(self):
|
||||
self.assertEqual(
|
||||
'{"some":"data"}', self.de.loads('{"some":"data"}', "text/plain")
|
||||
)
|
||||
self.assertEqual(
|
||||
'{"some":"data"}',
|
||||
self.de.loads('{"some":"data"}', "text/plain; charset=whatever"),
|
||||
)
|
||||
|
||||
def test_raises_serialization_error_on_unknown_mimetype(self):
|
||||
self.assertRaises(SerializationError, self.de.loads, "{}", "text/html")
|
||||
|
||||
def test_raises_improperly_configured_when_default_mimetype_cannot_be_deserialized(
|
||||
self,
|
||||
):
|
||||
self.assertRaises(ImproperlyConfigured, Deserializer, {})
|
||||
@@ -0,0 +1,68 @@
|
||||
# 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.
|
||||
|
||||
from unittest import SkipTest
|
||||
|
||||
from opensearchpy.helpers import test
|
||||
from opensearchpy.helpers.test import OpenSearchTestCase as BaseTestCase
|
||||
|
||||
client = None
|
||||
|
||||
|
||||
def get_client(**kwargs):
|
||||
global client
|
||||
if client is False:
|
||||
raise SkipTest("No client is available")
|
||||
if client is not None and not kwargs:
|
||||
return client
|
||||
|
||||
# try and locate manual override in the local environment
|
||||
try:
|
||||
from test_opensearchpy.local import get_client as local_get_client
|
||||
|
||||
new_client = local_get_client(**kwargs)
|
||||
except ImportError:
|
||||
# fallback to using vanilla client
|
||||
try:
|
||||
new_client = test.get_test_client(**kwargs)
|
||||
except SkipTest:
|
||||
client = False
|
||||
raise
|
||||
|
||||
if not kwargs:
|
||||
client = new_client
|
||||
|
||||
return new_client
|
||||
|
||||
|
||||
def setup_module():
|
||||
get_client()
|
||||
|
||||
|
||||
class OpenSearchTestCase(BaseTestCase):
|
||||
@staticmethod
|
||||
def _get_client(**kwargs):
|
||||
return get_client(**kwargs)
|
||||
@@ -0,0 +1,90 @@
|
||||
# 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 time
|
||||
|
||||
import pytest
|
||||
|
||||
import opensearchpy
|
||||
from opensearchpy.helpers.test import CA_CERTS, OPENSEARCH_URL
|
||||
|
||||
from ..utils import wipe_cluster
|
||||
|
||||
# Information about the OpenSearch instance running, if any
|
||||
# Used for
|
||||
OPENSEARCH_VERSION = ""
|
||||
OPENSEARCH_BUILD_HASH = ""
|
||||
OPENSEARCH_REST_API_TESTS = []
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def sync_client_factory():
|
||||
client = None
|
||||
try:
|
||||
# Configure the client with certificates and optionally
|
||||
# an HTTP conn class depending on 'PYTHON_CONNECTION_CLASS' envvar
|
||||
kw = {
|
||||
"timeout": 3,
|
||||
"ca_certs": CA_CERTS,
|
||||
"headers": {"Authorization": "Basic ZWxhc3RpYzpjaGFuZ2VtZQ=="},
|
||||
}
|
||||
if "PYTHON_CONNECTION_CLASS" in os.environ:
|
||||
from opensearchpy import connection
|
||||
|
||||
kw["connection_class"] = getattr(
|
||||
connection, os.environ["PYTHON_CONNECTION_CLASS"]
|
||||
)
|
||||
|
||||
# We do this little dance with the URL to force
|
||||
# Requests to respect 'headers: None' within rest API spec tests.
|
||||
client = opensearchpy.OpenSearch(
|
||||
OPENSEARCH_URL.replace("elastic:changeme@", ""), **kw
|
||||
)
|
||||
|
||||
# Wait for the cluster to report a status of 'yellow'
|
||||
for _ in range(100):
|
||||
try:
|
||||
client.cluster.health(wait_for_status="yellow")
|
||||
break
|
||||
except ConnectionError:
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
pytest.skip("OpenSearch wasn't running at %r" % (OPENSEARCH_URL,))
|
||||
|
||||
wipe_cluster(client)
|
||||
yield client
|
||||
finally:
|
||||
if client:
|
||||
client.close()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def sync_client(sync_client_factory):
|
||||
try:
|
||||
yield sync_client_factory
|
||||
finally:
|
||||
wipe_cluster(sync_client_factory)
|
||||
@@ -0,0 +1,51 @@
|
||||
# -*- 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.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from . import OpenSearchTestCase
|
||||
|
||||
|
||||
class TestUnicode(OpenSearchTestCase):
|
||||
def test_indices_analyze(self):
|
||||
self.client.indices.analyze(body='{"text": "привет"}')
|
||||
|
||||
|
||||
class TestBulk(OpenSearchTestCase):
|
||||
def test_bulk_works_with_string_body(self):
|
||||
docs = '{ "index" : { "_index" : "bulk_test_index", "_id" : "1" } }\n{"answer": 42}'
|
||||
response = self.client.bulk(body=docs)
|
||||
|
||||
self.assertFalse(response["errors"])
|
||||
self.assertEqual(1, len(response["items"]))
|
||||
|
||||
def test_bulk_works_with_bytestring_body(self):
|
||||
docs = b'{ "index" : { "_index" : "bulk_test_index", "_id" : "2" } }\n{"answer": 42}'
|
||||
response = self.client.bulk(body=docs)
|
||||
|
||||
self.assertFalse(response["errors"])
|
||||
self.assertEqual(1, len(response["items"]))
|
||||
@@ -0,0 +1,775 @@
|
||||
# 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.
|
||||
|
||||
from mock import patch
|
||||
|
||||
from opensearchpy import TransportError, helpers
|
||||
from opensearchpy.helpers import ScanError
|
||||
|
||||
from ..test_cases import SkipTest
|
||||
from . import OpenSearchTestCase
|
||||
|
||||
|
||||
class FailingBulkClient(object):
|
||||
def __init__(
|
||||
self, client, fail_at=(2,), fail_with=TransportError(599, "Error!", {})
|
||||
):
|
||||
self.client = client
|
||||
self._called = 0
|
||||
self._fail_at = fail_at
|
||||
self.transport = client.transport
|
||||
self._fail_with = fail_with
|
||||
|
||||
def bulk(self, *args, **kwargs):
|
||||
self._called += 1
|
||||
if self._called in self._fail_at:
|
||||
raise self._fail_with
|
||||
return self.client.bulk(*args, **kwargs)
|
||||
|
||||
|
||||
class TestStreamingBulk(OpenSearchTestCase):
|
||||
def test_actions_remain_unchanged(self):
|
||||
actions = [{"_id": 1}, {"_id": 2}]
|
||||
for ok, item in helpers.streaming_bulk(
|
||||
self.client, actions, index="test-index"
|
||||
):
|
||||
self.assertTrue(ok)
|
||||
self.assertEqual([{"_id": 1}, {"_id": 2}], actions)
|
||||
|
||||
def test_all_documents_get_inserted(self):
|
||||
docs = [{"answer": x, "_id": x} for x in range(100)]
|
||||
for ok, item in helpers.streaming_bulk(
|
||||
self.client, docs, index="test-index", refresh=True
|
||||
):
|
||||
self.assertTrue(ok)
|
||||
|
||||
self.assertEqual(100, self.client.count(index="test-index")["count"])
|
||||
self.assertEqual(
|
||||
{"answer": 42}, self.client.get(index="test-index", id=42)["_source"]
|
||||
)
|
||||
|
||||
def test_all_errors_from_chunk_are_raised_on_failure(self):
|
||||
self.client.indices.create(
|
||||
"i",
|
||||
{
|
||||
"mappings": {"properties": {"a": {"type": "integer"}}},
|
||||
"settings": {"number_of_shards": 1, "number_of_replicas": 0},
|
||||
},
|
||||
)
|
||||
self.client.cluster.health(wait_for_status="yellow")
|
||||
|
||||
try:
|
||||
for ok, item in helpers.streaming_bulk(
|
||||
self.client, [{"a": "b"}, {"a": "c"}], index="i", raise_on_error=True
|
||||
):
|
||||
self.assertTrue(ok)
|
||||
except helpers.BulkIndexError as e:
|
||||
self.assertEqual(2, len(e.errors))
|
||||
else:
|
||||
assert False, "exception should have been raised"
|
||||
|
||||
def test_different_op_types(self):
|
||||
if self.opensearch_version() < (0, 90, 1):
|
||||
raise SkipTest("update supported since 0.90.1")
|
||||
self.client.index(index="i", id=45, body={})
|
||||
self.client.index(index="i", id=42, body={})
|
||||
docs = [
|
||||
{"_index": "i", "_type": "_doc", "_id": 47, "f": "v"},
|
||||
{"_op_type": "delete", "_index": "i", "_type": "_doc", "_id": 45},
|
||||
{
|
||||
"_op_type": "update",
|
||||
"_index": "i",
|
||||
"_type": "_doc",
|
||||
"_id": 42,
|
||||
"doc": {"answer": 42},
|
||||
},
|
||||
]
|
||||
for ok, item in helpers.streaming_bulk(self.client, docs):
|
||||
self.assertTrue(ok)
|
||||
|
||||
self.assertFalse(self.client.exists(index="i", id=45))
|
||||
self.assertEqual({"answer": 42}, self.client.get(index="i", id=42)["_source"])
|
||||
self.assertEqual({"f": "v"}, self.client.get(index="i", id=47)["_source"])
|
||||
|
||||
def test_transport_error_can_becaught(self):
|
||||
failing_client = FailingBulkClient(self.client)
|
||||
docs = [
|
||||
{"_index": "i", "_type": "_doc", "_id": 47, "f": "v"},
|
||||
{"_index": "i", "_type": "_doc", "_id": 45, "f": "v"},
|
||||
{"_index": "i", "_type": "_doc", "_id": 42, "f": "v"},
|
||||
]
|
||||
|
||||
results = list(
|
||||
helpers.streaming_bulk(
|
||||
failing_client,
|
||||
docs,
|
||||
raise_on_exception=False,
|
||||
raise_on_error=False,
|
||||
chunk_size=1,
|
||||
)
|
||||
)
|
||||
self.assertEqual(3, len(results))
|
||||
self.assertEqual([True, False, True], [r[0] for r in results])
|
||||
|
||||
exc = results[1][1]["index"].pop("exception")
|
||||
self.assertIsInstance(exc, TransportError)
|
||||
self.assertEqual(599, exc.status_code)
|
||||
self.assertEqual(
|
||||
{
|
||||
"index": {
|
||||
"_index": "i",
|
||||
"_type": "_doc",
|
||||
"_id": 45,
|
||||
"data": {"f": "v"},
|
||||
"error": "TransportError(599, 'Error!')",
|
||||
"status": 599,
|
||||
}
|
||||
},
|
||||
results[1][1],
|
||||
)
|
||||
|
||||
def test_rejected_documents_are_retried(self):
|
||||
failing_client = FailingBulkClient(
|
||||
self.client, fail_with=TransportError(429, "Rejected!", {})
|
||||
)
|
||||
docs = [
|
||||
{"_index": "i", "_type": "_doc", "_id": 47, "f": "v"},
|
||||
{"_index": "i", "_type": "_doc", "_id": 45, "f": "v"},
|
||||
{"_index": "i", "_type": "_doc", "_id": 42, "f": "v"},
|
||||
]
|
||||
results = list(
|
||||
helpers.streaming_bulk(
|
||||
failing_client,
|
||||
docs,
|
||||
raise_on_exception=False,
|
||||
raise_on_error=False,
|
||||
chunk_size=1,
|
||||
max_retries=1,
|
||||
initial_backoff=0,
|
||||
)
|
||||
)
|
||||
self.assertEqual(3, len(results))
|
||||
self.assertEqual([True, True, True], [r[0] for r in results])
|
||||
self.client.indices.refresh(index="i")
|
||||
res = self.client.search(index="i")
|
||||
self.assertEqual({"value": 3, "relation": "eq"}, res["hits"]["total"])
|
||||
self.assertEqual(4, failing_client._called)
|
||||
|
||||
def test_rejected_documents_are_retried_at_most_max_retries_times(self):
|
||||
failing_client = FailingBulkClient(
|
||||
self.client, fail_at=(1, 2), fail_with=TransportError(429, "Rejected!", {})
|
||||
)
|
||||
|
||||
docs = [
|
||||
{"_index": "i", "_type": "_doc", "_id": 47, "f": "v"},
|
||||
{"_index": "i", "_type": "_doc", "_id": 45, "f": "v"},
|
||||
{"_index": "i", "_type": "_doc", "_id": 42, "f": "v"},
|
||||
]
|
||||
results = list(
|
||||
helpers.streaming_bulk(
|
||||
failing_client,
|
||||
docs,
|
||||
raise_on_exception=False,
|
||||
raise_on_error=False,
|
||||
chunk_size=1,
|
||||
max_retries=1,
|
||||
initial_backoff=0,
|
||||
)
|
||||
)
|
||||
self.assertEqual(3, len(results))
|
||||
self.assertEqual([False, True, True], [r[0] for r in results])
|
||||
self.client.indices.refresh(index="i")
|
||||
res = self.client.search(index="i")
|
||||
self.assertEqual({"value": 2, "relation": "eq"}, res["hits"]["total"])
|
||||
self.assertEqual(4, failing_client._called)
|
||||
|
||||
def test_transport_error_is_raised_with_max_retries(self):
|
||||
failing_client = FailingBulkClient(
|
||||
self.client,
|
||||
fail_at=(1, 2, 3, 4),
|
||||
fail_with=TransportError(429, "Rejected!", {}),
|
||||
)
|
||||
|
||||
def streaming_bulk():
|
||||
results = list(
|
||||
helpers.streaming_bulk(
|
||||
failing_client,
|
||||
[{"a": 42}, {"a": 39}],
|
||||
raise_on_exception=True,
|
||||
max_retries=3,
|
||||
initial_backoff=0,
|
||||
)
|
||||
)
|
||||
return results
|
||||
|
||||
self.assertRaises(TransportError, streaming_bulk)
|
||||
self.assertEqual(4, failing_client._called)
|
||||
|
||||
|
||||
class TestBulk(OpenSearchTestCase):
|
||||
def test_bulk_works_with_single_item(self):
|
||||
docs = [{"answer": 42, "_id": 1}]
|
||||
success, failed = helpers.bulk(
|
||||
self.client, docs, index="test-index", refresh=True
|
||||
)
|
||||
|
||||
self.assertEqual(1, success)
|
||||
self.assertFalse(failed)
|
||||
self.assertEqual(1, self.client.count(index="test-index")["count"])
|
||||
self.assertEqual(
|
||||
{"answer": 42}, self.client.get(index="test-index", id=1)["_source"]
|
||||
)
|
||||
|
||||
def test_all_documents_get_inserted(self):
|
||||
docs = [{"answer": x, "_id": x} for x in range(100)]
|
||||
success, failed = helpers.bulk(
|
||||
self.client, docs, index="test-index", refresh=True
|
||||
)
|
||||
|
||||
self.assertEqual(100, success)
|
||||
self.assertFalse(failed)
|
||||
self.assertEqual(100, self.client.count(index="test-index")["count"])
|
||||
self.assertEqual(
|
||||
{"answer": 42}, self.client.get(index="test-index", id=42)["_source"]
|
||||
)
|
||||
|
||||
def test_stats_only_reports_numbers(self):
|
||||
docs = [{"answer": x} for x in range(100)]
|
||||
success, failed = helpers.bulk(
|
||||
self.client, docs, index="test-index", refresh=True, stats_only=True
|
||||
)
|
||||
|
||||
self.assertEqual(100, success)
|
||||
self.assertEqual(0, failed)
|
||||
self.assertEqual(100, self.client.count(index="test-index")["count"])
|
||||
|
||||
def test_errors_are_reported_correctly(self):
|
||||
self.client.indices.create(
|
||||
"i",
|
||||
{
|
||||
"mappings": {"properties": {"a": {"type": "integer"}}},
|
||||
"settings": {"number_of_shards": 1, "number_of_replicas": 0},
|
||||
},
|
||||
)
|
||||
self.client.cluster.health(wait_for_status="yellow")
|
||||
|
||||
success, failed = helpers.bulk(
|
||||
self.client,
|
||||
[{"a": 42}, {"a": "c", "_id": 42}],
|
||||
index="i",
|
||||
raise_on_error=False,
|
||||
)
|
||||
self.assertEqual(1, success)
|
||||
self.assertEqual(1, len(failed))
|
||||
error = failed[0]
|
||||
self.assertEqual("42", error["index"]["_id"])
|
||||
self.assertEqual("_doc", error["index"]["_type"])
|
||||
self.assertEqual("i", error["index"]["_index"])
|
||||
print(error["index"]["error"])
|
||||
self.assertTrue(
|
||||
"MapperParsingException" in repr(error["index"]["error"])
|
||||
or "mapper_parsing_exception" in repr(error["index"]["error"])
|
||||
)
|
||||
|
||||
def test_error_is_raised(self):
|
||||
self.client.indices.create(
|
||||
"i",
|
||||
{
|
||||
"mappings": {"properties": {"a": {"type": "integer"}}},
|
||||
"settings": {"number_of_shards": 1, "number_of_replicas": 0},
|
||||
},
|
||||
)
|
||||
self.client.cluster.health(wait_for_status="yellow")
|
||||
|
||||
self.assertRaises(
|
||||
helpers.BulkIndexError,
|
||||
helpers.bulk,
|
||||
self.client,
|
||||
[{"a": 42}, {"a": "c"}],
|
||||
index="i",
|
||||
)
|
||||
|
||||
def test_ignore_error_if_raised(self):
|
||||
# ignore the status code 400 in tuple
|
||||
helpers.bulk(
|
||||
self.client, [{"a": 42}, {"a": "c"}], index="i", ignore_status=(400,)
|
||||
)
|
||||
|
||||
# ignore the status code 400 in list
|
||||
helpers.bulk(
|
||||
self.client,
|
||||
[{"a": 42}, {"a": "c"}],
|
||||
index="i",
|
||||
ignore_status=[
|
||||
400,
|
||||
],
|
||||
)
|
||||
|
||||
# ignore the status code 400
|
||||
helpers.bulk(self.client, [{"a": 42}, {"a": "c"}], index="i", ignore_status=400)
|
||||
|
||||
# ignore only the status code in the `ignore_status` argument
|
||||
self.assertRaises(
|
||||
helpers.BulkIndexError,
|
||||
helpers.bulk,
|
||||
self.client,
|
||||
[{"a": 42}, {"a": "c"}],
|
||||
index="i",
|
||||
ignore_status=(444,),
|
||||
)
|
||||
|
||||
# ignore transport error exception
|
||||
failing_client = FailingBulkClient(self.client)
|
||||
helpers.bulk(failing_client, [{"a": 42}], index="i", ignore_status=(599,))
|
||||
|
||||
def test_errors_are_collected_properly(self):
|
||||
self.client.indices.create(
|
||||
"i",
|
||||
{
|
||||
"mappings": {"properties": {"a": {"type": "integer"}}},
|
||||
"settings": {"number_of_shards": 1, "number_of_replicas": 0},
|
||||
},
|
||||
)
|
||||
self.client.cluster.health(wait_for_status="yellow")
|
||||
|
||||
success, failed = helpers.bulk(
|
||||
self.client,
|
||||
[{"a": 42}, {"a": "c"}],
|
||||
index="i",
|
||||
stats_only=True,
|
||||
raise_on_error=False,
|
||||
)
|
||||
self.assertEqual(1, success)
|
||||
self.assertEqual(1, failed)
|
||||
|
||||
|
||||
class TestScan(OpenSearchTestCase):
|
||||
mock_scroll_responses = [
|
||||
{
|
||||
"_scroll_id": "dummy_id",
|
||||
"_shards": {"successful": 4, "total": 5, "skipped": 0},
|
||||
"hits": {"hits": [{"scroll_data": 42}]},
|
||||
},
|
||||
{
|
||||
"_scroll_id": "dummy_id",
|
||||
"_shards": {"successful": 4, "total": 5, "skipped": 0},
|
||||
"hits": {"hits": []},
|
||||
},
|
||||
]
|
||||
|
||||
def teardown_method(self, m):
|
||||
self.client.transport.perform_request("DELETE", "/_search/scroll/_all")
|
||||
super(TestScan, self).teardown_method(m)
|
||||
|
||||
def test_order_can_be_preserved(self):
|
||||
bulk = []
|
||||
for x in range(100):
|
||||
bulk.append({"index": {"_index": "test_index", "_type": "_doc", "_id": x}})
|
||||
bulk.append({"answer": x, "correct": x == 42})
|
||||
self.client.bulk(bulk, refresh=True)
|
||||
|
||||
docs = list(
|
||||
helpers.scan(
|
||||
self.client,
|
||||
index="test_index",
|
||||
query={"sort": "answer"},
|
||||
preserve_order=True,
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(100, len(docs))
|
||||
self.assertEqual(list(map(str, range(100))), list(d["_id"] for d in docs))
|
||||
self.assertEqual(list(range(100)), list(d["_source"]["answer"] for d in docs))
|
||||
|
||||
def test_all_documents_are_read(self):
|
||||
bulk = []
|
||||
for x in range(100):
|
||||
bulk.append({"index": {"_index": "test_index", "_type": "_doc", "_id": x}})
|
||||
bulk.append({"answer": x, "correct": x == 42})
|
||||
self.client.bulk(bulk, refresh=True)
|
||||
|
||||
docs = list(helpers.scan(self.client, index="test_index", size=2))
|
||||
|
||||
self.assertEqual(100, len(docs))
|
||||
self.assertEqual(set(map(str, range(100))), set(d["_id"] for d in docs))
|
||||
self.assertEqual(set(range(100)), set(d["_source"]["answer"] for d in docs))
|
||||
|
||||
def test_scroll_error(self):
|
||||
bulk = []
|
||||
for x in range(4):
|
||||
bulk.append({"index": {"_index": "test_index", "_type": "_doc"}})
|
||||
bulk.append({"value": x})
|
||||
self.client.bulk(bulk, refresh=True)
|
||||
|
||||
with patch.object(self.client, "scroll") as scroll_mock:
|
||||
scroll_mock.side_effect = self.mock_scroll_responses
|
||||
data = list(
|
||||
helpers.scan(
|
||||
self.client,
|
||||
index="test_index",
|
||||
size=2,
|
||||
raise_on_error=False,
|
||||
clear_scroll=False,
|
||||
)
|
||||
)
|
||||
self.assertEqual(len(data), 3)
|
||||
self.assertEqual(data[-1], {"scroll_data": 42})
|
||||
|
||||
scroll_mock.side_effect = self.mock_scroll_responses
|
||||
with self.assertRaises(ScanError):
|
||||
data = list(
|
||||
helpers.scan(
|
||||
self.client,
|
||||
index="test_index",
|
||||
size=2,
|
||||
raise_on_error=True,
|
||||
clear_scroll=False,
|
||||
)
|
||||
)
|
||||
self.assertEqual(len(data), 3)
|
||||
self.assertEqual(data[-1], {"scroll_data": 42})
|
||||
|
||||
def test_initial_search_error(self):
|
||||
with patch.object(self, "client") as client_mock:
|
||||
client_mock.search.return_value = {
|
||||
"_scroll_id": "dummy_id",
|
||||
"_shards": {"successful": 4, "total": 5, "skipped": 0},
|
||||
"hits": {"hits": [{"search_data": 1}]},
|
||||
}
|
||||
client_mock.scroll.side_effect = self.mock_scroll_responses
|
||||
|
||||
data = list(
|
||||
helpers.scan(
|
||||
self.client, index="test_index", size=2, raise_on_error=False
|
||||
)
|
||||
)
|
||||
self.assertEqual(data, [{"search_data": 1}, {"scroll_data": 42}])
|
||||
|
||||
client_mock.scroll.side_effect = self.mock_scroll_responses
|
||||
with self.assertRaises(ScanError):
|
||||
data = list(
|
||||
helpers.scan(
|
||||
self.client, index="test_index", size=2, raise_on_error=True
|
||||
)
|
||||
)
|
||||
self.assertEqual(data, [{"search_data": 1}])
|
||||
client_mock.scroll.assert_not_called()
|
||||
|
||||
def test_no_scroll_id_fast_route(self):
|
||||
with patch.object(self, "client") as client_mock:
|
||||
client_mock.search.return_value = {"no": "_scroll_id"}
|
||||
data = list(helpers.scan(self.client, index="test_index"))
|
||||
|
||||
self.assertEqual(data, [])
|
||||
client_mock.scroll.assert_not_called()
|
||||
client_mock.clear_scroll.assert_not_called()
|
||||
|
||||
def test_scan_auth_kwargs_forwarded(self):
|
||||
for key, val in {
|
||||
"api_key": ("name", "value"),
|
||||
"http_auth": ("username", "password"),
|
||||
"headers": {"custom": "header"},
|
||||
}.items():
|
||||
with patch.object(self, "client") as client_mock:
|
||||
client_mock.search.return_value = {
|
||||
"_scroll_id": "scroll_id",
|
||||
"_shards": {"successful": 5, "total": 5, "skipped": 0},
|
||||
"hits": {"hits": [{"search_data": 1}]},
|
||||
}
|
||||
client_mock.scroll.return_value = {
|
||||
"_scroll_id": "scroll_id",
|
||||
"_shards": {"successful": 5, "total": 5, "skipped": 0},
|
||||
"hits": {"hits": []},
|
||||
}
|
||||
client_mock.clear_scroll.return_value = {}
|
||||
|
||||
data = list(helpers.scan(self.client, index="test_index", **{key: val}))
|
||||
|
||||
self.assertEqual(data, [{"search_data": 1}])
|
||||
|
||||
# Assert that 'search', 'scroll' and 'clear_scroll' all
|
||||
# received the extra kwarg related to authentication.
|
||||
for api_mock in (
|
||||
client_mock.search,
|
||||
client_mock.scroll,
|
||||
client_mock.clear_scroll,
|
||||
):
|
||||
self.assertEqual(api_mock.call_args[1][key], val)
|
||||
|
||||
def test_scan_auth_kwargs_favor_scroll_kwargs_option(self):
|
||||
with patch.object(self, "client") as client_mock:
|
||||
client_mock.search.return_value = {
|
||||
"_scroll_id": "scroll_id",
|
||||
"_shards": {"successful": 5, "total": 5, "skipped": 0},
|
||||
"hits": {"hits": [{"search_data": 1}]},
|
||||
}
|
||||
client_mock.scroll.return_value = {
|
||||
"_scroll_id": "scroll_id",
|
||||
"_shards": {"successful": 5, "total": 5, "skipped": 0},
|
||||
"hits": {"hits": []},
|
||||
}
|
||||
client_mock.clear_scroll.return_value = {}
|
||||
|
||||
data = list(
|
||||
helpers.scan(
|
||||
self.client,
|
||||
index="test_index",
|
||||
scroll_kwargs={"headers": {"scroll": "kwargs"}, "sort": "asc"},
|
||||
headers={"not scroll": "kwargs"},
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(data, [{"search_data": 1}])
|
||||
|
||||
# Assert that we see 'scroll_kwargs' options used instead of 'kwargs'
|
||||
self.assertEqual(
|
||||
client_mock.scroll.call_args[1]["headers"], {"scroll": "kwargs"}
|
||||
)
|
||||
self.assertEqual(client_mock.scroll.call_args[1]["sort"], "asc")
|
||||
|
||||
@patch("opensearchpy.helpers.actions.logger")
|
||||
def test_logger(self, logger_mock):
|
||||
bulk = []
|
||||
for x in range(4):
|
||||
bulk.append({"index": {"_index": "test_index", "_type": "_doc"}})
|
||||
bulk.append({"value": x})
|
||||
self.client.bulk(bulk, refresh=True)
|
||||
|
||||
with patch.object(self.client, "scroll") as scroll_mock:
|
||||
scroll_mock.side_effect = self.mock_scroll_responses
|
||||
list(
|
||||
helpers.scan(
|
||||
self.client,
|
||||
index="test_index",
|
||||
size=2,
|
||||
raise_on_error=False,
|
||||
clear_scroll=False,
|
||||
)
|
||||
)
|
||||
logger_mock.warning.assert_called()
|
||||
|
||||
scroll_mock.side_effect = self.mock_scroll_responses
|
||||
try:
|
||||
list(
|
||||
helpers.scan(
|
||||
self.client,
|
||||
index="test_index",
|
||||
size=2,
|
||||
raise_on_error=True,
|
||||
clear_scroll=False,
|
||||
)
|
||||
)
|
||||
except ScanError:
|
||||
pass
|
||||
logger_mock.warning.assert_called()
|
||||
|
||||
def test_clear_scroll(self):
|
||||
bulk = []
|
||||
for x in range(4):
|
||||
bulk.append({"index": {"_index": "test_index", "_type": "_doc"}})
|
||||
bulk.append({"value": x})
|
||||
self.client.bulk(bulk, refresh=True)
|
||||
|
||||
with patch.object(
|
||||
self.client, "clear_scroll", wraps=self.client.clear_scroll
|
||||
) as spy:
|
||||
list(helpers.scan(self.client, index="test_index", size=2))
|
||||
spy.assert_called_once()
|
||||
|
||||
spy.reset_mock()
|
||||
list(
|
||||
helpers.scan(self.client, index="test_index", size=2, clear_scroll=True)
|
||||
)
|
||||
spy.assert_called_once()
|
||||
|
||||
spy.reset_mock()
|
||||
list(
|
||||
helpers.scan(
|
||||
self.client, index="test_index", size=2, clear_scroll=False
|
||||
)
|
||||
)
|
||||
spy.assert_not_called()
|
||||
|
||||
def test_shards_no_skipped_field(self):
|
||||
with patch.object(self, "client") as client_mock:
|
||||
client_mock.search.return_value = {
|
||||
"_scroll_id": "dummy_id",
|
||||
"_shards": {"successful": 5, "total": 5},
|
||||
"hits": {"hits": [{"search_data": 1}]},
|
||||
}
|
||||
client_mock.scroll.side_effect = [
|
||||
{
|
||||
"_scroll_id": "dummy_id",
|
||||
"_shards": {"successful": 5, "total": 5},
|
||||
"hits": {"hits": [{"scroll_data": 42}]},
|
||||
},
|
||||
{
|
||||
"_scroll_id": "dummy_id",
|
||||
"_shards": {"successful": 5, "total": 5},
|
||||
"hits": {"hits": []},
|
||||
},
|
||||
]
|
||||
|
||||
data = list(
|
||||
helpers.scan(
|
||||
self.client, index="test_index", size=2, raise_on_error=True
|
||||
)
|
||||
)
|
||||
self.assertEqual(data, [{"search_data": 1}, {"scroll_data": 42}])
|
||||
|
||||
|
||||
class TestReindex(OpenSearchTestCase):
|
||||
def setup_method(self, _):
|
||||
bulk = []
|
||||
for x in range(100):
|
||||
bulk.append({"index": {"_index": "test_index", "_type": "_doc", "_id": x}})
|
||||
bulk.append(
|
||||
{
|
||||
"answer": x,
|
||||
"correct": x == 42,
|
||||
"type": "answers" if x % 2 == 0 else "questions",
|
||||
}
|
||||
)
|
||||
self.client.bulk(bulk, refresh=True)
|
||||
|
||||
def test_reindex_passes_kwargs_to_scan_and_bulk(self):
|
||||
helpers.reindex(
|
||||
self.client,
|
||||
"test_index",
|
||||
"prod_index",
|
||||
scan_kwargs={"q": "type:answers"},
|
||||
bulk_kwargs={"refresh": True},
|
||||
)
|
||||
|
||||
self.assertTrue(self.client.indices.exists("prod_index"))
|
||||
self.assertEqual(
|
||||
50, self.client.count(index="prod_index", q="type:answers")["count"]
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
{"answer": 42, "correct": True, "type": "answers"},
|
||||
self.client.get(index="prod_index", id=42)["_source"],
|
||||
)
|
||||
|
||||
def test_reindex_accepts_a_query(self):
|
||||
helpers.reindex(
|
||||
self.client,
|
||||
"test_index",
|
||||
"prod_index",
|
||||
query={"query": {"bool": {"filter": {"term": {"type": "answers"}}}}},
|
||||
)
|
||||
self.client.indices.refresh()
|
||||
|
||||
self.assertTrue(self.client.indices.exists("prod_index"))
|
||||
self.assertEqual(
|
||||
50, self.client.count(index="prod_index", q="type:answers")["count"]
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
{"answer": 42, "correct": True, "type": "answers"},
|
||||
self.client.get(index="prod_index", id=42)["_source"],
|
||||
)
|
||||
|
||||
def test_all_documents_get_moved(self):
|
||||
helpers.reindex(self.client, "test_index", "prod_index")
|
||||
self.client.indices.refresh()
|
||||
|
||||
self.assertTrue(self.client.indices.exists("prod_index"))
|
||||
self.assertEqual(
|
||||
50, self.client.count(index="prod_index", q="type:questions")["count"]
|
||||
)
|
||||
self.assertEqual(
|
||||
50, self.client.count(index="prod_index", q="type:answers")["count"]
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
{"answer": 42, "correct": True, "type": "answers"},
|
||||
self.client.get(index="prod_index", id=42)["_source"],
|
||||
)
|
||||
|
||||
|
||||
class TestParentChildReindex(OpenSearchTestCase):
|
||||
def setup_method(self, _):
|
||||
body = {
|
||||
"settings": {"number_of_shards": 1, "number_of_replicas": 0},
|
||||
"mappings": {
|
||||
"properties": {
|
||||
"question_answer": {
|
||||
"type": "join",
|
||||
"relations": {"question": "answer"},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
self.client.indices.create(index="test-index", body=body)
|
||||
self.client.indices.create(index="real-index", body=body)
|
||||
|
||||
self.client.index(
|
||||
index="test-index", id=42, body={"question_answer": "question"}
|
||||
)
|
||||
self.client.index(
|
||||
index="test-index",
|
||||
id=47,
|
||||
routing=42,
|
||||
body={"some": "data", "question_answer": {"name": "answer", "parent": 42}},
|
||||
)
|
||||
self.client.indices.refresh(index="test-index")
|
||||
|
||||
def test_children_are_reindexed_correctly(self):
|
||||
helpers.reindex(self.client, "test-index", "real-index")
|
||||
|
||||
q = self.client.get(index="real-index", id=42)
|
||||
self.assertEqual(
|
||||
{
|
||||
"_id": "42",
|
||||
"_index": "real-index",
|
||||
"_primary_term": 1,
|
||||
"_seq_no": 0,
|
||||
"_source": {"question_answer": "question"},
|
||||
"_type": "_doc",
|
||||
"_version": 1,
|
||||
"found": True,
|
||||
},
|
||||
q,
|
||||
)
|
||||
q = self.client.get(index="test-index", id=47, routing=42)
|
||||
self.assertEqual(
|
||||
{
|
||||
"_routing": "42",
|
||||
"_id": "47",
|
||||
"_index": "test-index",
|
||||
"_primary_term": 1,
|
||||
"_seq_no": 1,
|
||||
"_source": {
|
||||
"some": "data",
|
||||
"question_answer": {"name": "answer", "parent": 42},
|
||||
},
|
||||
"_type": "_doc",
|
||||
"_version": 1,
|
||||
"found": True,
|
||||
},
|
||||
q,
|
||||
)
|
||||
@@ -0,0 +1,566 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Dynamically generated set of TestCases based on set of yaml files describing
|
||||
some integration tests. These files are shared among all official OpenSearch
|
||||
clients.
|
||||
"""
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import warnings
|
||||
import zipfile
|
||||
|
||||
import pytest
|
||||
import urllib3
|
||||
import yaml
|
||||
|
||||
from opensearchpy import OpenSearchWarning, TransportError
|
||||
from opensearchpy.client.utils import _base64_auth_header
|
||||
from opensearchpy.compat import string_types
|
||||
from opensearchpy.helpers.test import _get_version
|
||||
|
||||
from . import get_client
|
||||
|
||||
# some params had to be changed in python, keep track of them so we can rename
|
||||
# those in the tests accordingly
|
||||
PARAMS_RENAMES = {"type": "doc_type", "from": "from_"}
|
||||
|
||||
# mapping from catch values to http status codes
|
||||
CATCH_CODES = {"missing": 404, "conflict": 409, "unauthorized": 401}
|
||||
|
||||
# test features we have implemented
|
||||
IMPLEMENTED_FEATURES = {
|
||||
"gtelte",
|
||||
"stash_in_path",
|
||||
"headers",
|
||||
"catch_unauthorized",
|
||||
"default_shards",
|
||||
"warnings",
|
||||
"allowed_warnings",
|
||||
"contains",
|
||||
"arbitrary_key",
|
||||
"transform_and_set",
|
||||
}
|
||||
|
||||
# broken YAML tests on some releases
|
||||
SKIP_TESTS = {
|
||||
# Warning about date_histogram.interval deprecation is raised randomly
|
||||
"search/aggregation/250_moving_fn[1]",
|
||||
# body: null
|
||||
"indices/simulate_index_template/10_basic[2]",
|
||||
# No ML node with sufficient capacity / random ML failing
|
||||
"ml/start_stop_datafeed",
|
||||
"ml/post_data",
|
||||
"ml/jobs_crud",
|
||||
"ml/datafeeds_crud",
|
||||
"ml/set_upgrade_mode",
|
||||
"ml/reset_job[2]",
|
||||
"ml/jobs_get_stats",
|
||||
"ml/get_datafeed_stats",
|
||||
"ml/get_trained_model_stats",
|
||||
"ml/delete_job_force",
|
||||
"ml/jobs_get_result_overall_buckets",
|
||||
"ml/bucket_correlation_agg[0]",
|
||||
"ml/job_groups",
|
||||
"transform/transforms_stats_continuous[0]",
|
||||
# Fails bad request instead of 404?
|
||||
"ml/inference_crud",
|
||||
# Our TLS certs are custom
|
||||
"ssl/10_basic[0]",
|
||||
# Our user is custom
|
||||
"users/10_basic[3]",
|
||||
# Shards/snapshots aren't right?
|
||||
"searchable_snapshots/10_usage[1]",
|
||||
# flaky data streams?
|
||||
"data_stream/10_basic[1]",
|
||||
"data_stream/80_resolve_index_data_streams[1]",
|
||||
# bad formatting?
|
||||
"cat/allocation/10_basic",
|
||||
# service account number not right?
|
||||
"service_accounts/10_basic[1]",
|
||||
# doesn't use 'contains' properly?
|
||||
"privileges/40_get_user_privs[0]",
|
||||
"privileges/40_get_user_privs[1]",
|
||||
# bad use of 'is_false'?
|
||||
"indices/get_alias/10_basic[22]",
|
||||
# unique usage of 'set'
|
||||
"indices/stats/50_disk_usage[0]",
|
||||
"indices/stats/60_field_usage[0]",
|
||||
}
|
||||
|
||||
|
||||
OPENSEARCH_VERSION = None
|
||||
RUN_ASYNC_REST_API_TESTS = (
|
||||
sys.version_info >= (3, 6)
|
||||
and os.environ.get("PYTHON_CONNECTION_CLASS") == "RequestsHttpConnection"
|
||||
)
|
||||
|
||||
FALSEY_VALUES = ("", None, False, 0, 0.0)
|
||||
|
||||
|
||||
class YamlRunner:
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
self.last_response = None
|
||||
|
||||
self._run_code = None
|
||||
self._setup_code = None
|
||||
self._teardown_code = None
|
||||
self._state = {}
|
||||
|
||||
def use_spec(self, test_spec):
|
||||
self._setup_code = test_spec.pop("setup", None)
|
||||
self._run_code = test_spec.pop("run", None)
|
||||
self._teardown_code = test_spec.pop("teardown", None)
|
||||
|
||||
def setup(self):
|
||||
# Pull skips from individual tests to not do unnecessary setup.
|
||||
skip_code = []
|
||||
for action in self._run_code:
|
||||
assert len(action) == 1
|
||||
action_type, _ = list(action.items())[0]
|
||||
if action_type == "skip":
|
||||
skip_code.append(action)
|
||||
else:
|
||||
break
|
||||
|
||||
if self._setup_code or skip_code:
|
||||
self.section("setup")
|
||||
if skip_code:
|
||||
self.run_code(skip_code)
|
||||
if self._setup_code:
|
||||
self.run_code(self._setup_code)
|
||||
|
||||
def teardown(self):
|
||||
if self._teardown_code:
|
||||
self.section("teardown")
|
||||
self.run_code(self._teardown_code)
|
||||
|
||||
def opensearch_version(self):
|
||||
global OPENSEARCH_VERSION
|
||||
if OPENSEARCH_VERSION is None:
|
||||
version_string = (self.client.info())["version"]["number"]
|
||||
if "." not in version_string:
|
||||
return ()
|
||||
version = version_string.strip().split(".")
|
||||
OPENSEARCH_VERSION = tuple(int(v) if v.isdigit() else 999 for v in version)
|
||||
return OPENSEARCH_VERSION
|
||||
|
||||
def section(self, name):
|
||||
print(("=" * 10) + " " + name + " " + ("=" * 10))
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.setup()
|
||||
self.section("test")
|
||||
self.run_code(self._run_code)
|
||||
finally:
|
||||
try:
|
||||
self.teardown()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def run_code(self, test):
|
||||
"""Execute an instruction based on it's type."""
|
||||
for action in test:
|
||||
assert len(action) == 1
|
||||
action_type, action = list(action.items())[0]
|
||||
print(action_type, action)
|
||||
|
||||
if hasattr(self, "run_" + action_type):
|
||||
getattr(self, "run_" + action_type)(action)
|
||||
else:
|
||||
raise RuntimeError("Invalid action type %r" % (action_type,))
|
||||
|
||||
def run_do(self, action):
|
||||
api = self.client
|
||||
headers = action.pop("headers", None)
|
||||
catch = action.pop("catch", None)
|
||||
warn = action.pop("warnings", ())
|
||||
allowed_warnings = action.pop("allowed_warnings", ())
|
||||
assert len(action) == 1
|
||||
|
||||
# Remove the x_pack_rest_user authentication
|
||||
# if it's given via headers. We're already authenticated
|
||||
# via the 'elastic' user.
|
||||
if (
|
||||
headers
|
||||
and headers.get("Authorization", None)
|
||||
== "Basic eF9wYWNrX3Jlc3RfdXNlcjp4LXBhY2stdGVzdC1wYXNzd29yZA=="
|
||||
):
|
||||
headers.pop("Authorization")
|
||||
|
||||
method, args = list(action.items())[0]
|
||||
args["headers"] = headers
|
||||
|
||||
# locate api endpoint
|
||||
for m in method.split("."):
|
||||
assert hasattr(api, m)
|
||||
api = getattr(api, m)
|
||||
|
||||
# some parameters had to be renamed to not clash with python builtins,
|
||||
# compensate
|
||||
for k in PARAMS_RENAMES:
|
||||
if k in args:
|
||||
args[PARAMS_RENAMES[k]] = args.pop(k)
|
||||
|
||||
# resolve vars
|
||||
for k in args:
|
||||
args[k] = self._resolve(args[k])
|
||||
|
||||
warnings.simplefilter("always", category=OpenSearchWarning)
|
||||
with warnings.catch_warnings(record=True) as caught_warnings:
|
||||
try:
|
||||
self.last_response = api(**args)
|
||||
except Exception as e:
|
||||
if not catch:
|
||||
raise
|
||||
self.run_catch(catch, e)
|
||||
else:
|
||||
if catch:
|
||||
raise AssertionError(
|
||||
"Failed to catch %r in %r." % (catch, self.last_response)
|
||||
)
|
||||
|
||||
# Filter out warnings raised by other components.
|
||||
caught_warnings = [
|
||||
str(w.message)
|
||||
for w in caught_warnings
|
||||
if w.category == OpenSearchWarning
|
||||
and str(w.message) not in allowed_warnings
|
||||
]
|
||||
|
||||
# This warning can show up in many places but isn't accounted for
|
||||
# in tests, so we remove it to make sure things pass.
|
||||
include_type_name_warning = (
|
||||
"[types removal] Using include_type_name in create index requests is deprecated. "
|
||||
"The parameter will be removed in the next major version."
|
||||
)
|
||||
if (
|
||||
include_type_name_warning in caught_warnings
|
||||
and include_type_name_warning not in warn
|
||||
):
|
||||
caught_warnings.remove(include_type_name_warning)
|
||||
|
||||
# Sorting removes the issue with order raised. We only care about
|
||||
# if all warnings are raised in the single API call.
|
||||
if warn and sorted(warn) != sorted(caught_warnings):
|
||||
raise AssertionError(
|
||||
"Expected warnings not equal to actual warnings: expected=%r actual=%r"
|
||||
% (warn, caught_warnings)
|
||||
)
|
||||
|
||||
def run_catch(self, catch, exception):
|
||||
if catch == "param":
|
||||
assert isinstance(exception, TypeError)
|
||||
return
|
||||
|
||||
assert isinstance(exception, TransportError)
|
||||
if catch in CATCH_CODES:
|
||||
assert CATCH_CODES[catch] == exception.status_code
|
||||
elif catch[0] == "/" and catch[-1] == "/":
|
||||
assert (
|
||||
re.search(catch[1:-1], exception.error + " " + repr(exception.info)),
|
||||
"%s not in %r" % (catch, exception.info),
|
||||
) is not None
|
||||
self.last_response = exception.info
|
||||
|
||||
def run_skip(self, skip):
|
||||
global IMPLEMENTED_FEATURES
|
||||
|
||||
if "features" in skip:
|
||||
features = skip["features"]
|
||||
if not isinstance(features, (tuple, list)):
|
||||
features = [features]
|
||||
for feature in features:
|
||||
if feature in IMPLEMENTED_FEATURES:
|
||||
continue
|
||||
pytest.skip("feature '%s' is not supported" % feature)
|
||||
|
||||
if "version" in skip:
|
||||
version, reason = skip["version"], skip["reason"]
|
||||
if version == "all":
|
||||
pytest.skip(reason)
|
||||
min_version, max_version = version.split("-")
|
||||
min_version = _get_version(min_version) or (0,)
|
||||
max_version = _get_version(max_version) or (999,)
|
||||
if min_version <= (self.opensearch_version()) <= max_version:
|
||||
pytest.skip(reason)
|
||||
|
||||
def run_gt(self, action):
|
||||
for key, value in action.items():
|
||||
value = self._resolve(value)
|
||||
assert self._lookup(key) > value
|
||||
|
||||
def run_gte(self, action):
|
||||
for key, value in action.items():
|
||||
value = self._resolve(value)
|
||||
assert self._lookup(key) >= value
|
||||
|
||||
def run_lt(self, action):
|
||||
for key, value in action.items():
|
||||
value = self._resolve(value)
|
||||
assert self._lookup(key) < value
|
||||
|
||||
def run_lte(self, action):
|
||||
for key, value in action.items():
|
||||
value = self._resolve(value)
|
||||
assert self._lookup(key) <= value
|
||||
|
||||
def run_set(self, action):
|
||||
for key, value in action.items():
|
||||
value = self._resolve(value)
|
||||
self._state[value] = self._lookup(key)
|
||||
|
||||
def run_is_false(self, action):
|
||||
try:
|
||||
value = self._lookup(action)
|
||||
except AssertionError:
|
||||
pass
|
||||
else:
|
||||
assert value in FALSEY_VALUES
|
||||
|
||||
def run_is_true(self, action):
|
||||
value = self._lookup(action)
|
||||
assert value not in FALSEY_VALUES
|
||||
|
||||
def run_length(self, action):
|
||||
for path, expected in action.items():
|
||||
value = self._lookup(path)
|
||||
expected = self._resolve(expected)
|
||||
assert expected == len(value)
|
||||
|
||||
def run_match(self, action):
|
||||
for path, expected in action.items():
|
||||
value = self._lookup(path)
|
||||
expected = self._resolve(expected)
|
||||
|
||||
if (
|
||||
isinstance(expected, string_types)
|
||||
and expected.startswith("/")
|
||||
and expected.endswith("/")
|
||||
):
|
||||
expected = re.compile(expected[1:-1], re.VERBOSE | re.MULTILINE)
|
||||
assert expected.search(value), "%r does not match %r" % (
|
||||
value,
|
||||
expected,
|
||||
)
|
||||
else:
|
||||
self._assert_match_equals(value, expected)
|
||||
|
||||
def run_contains(self, action):
|
||||
for path, expected in action.items():
|
||||
value = self._lookup(path) # list[dict[str,str]] is returned
|
||||
expected = self._resolve(expected) # dict[str, str]
|
||||
|
||||
if expected not in value:
|
||||
raise AssertionError("%s is not contained by %s" % (expected, value))
|
||||
|
||||
def run_transform_and_set(self, action):
|
||||
for key, value in action.items():
|
||||
# Convert #base64EncodeCredentials(id,api_key) to ["id", "api_key"]
|
||||
if "#base64EncodeCredentials" in value:
|
||||
value = value.replace("#base64EncodeCredentials", "")
|
||||
value = value.replace("(", "").replace(")", "").split(",")
|
||||
self._state[key] = _base64_auth_header(
|
||||
(self._lookup(value[0]), self._lookup(value[1]))
|
||||
)
|
||||
|
||||
def _resolve(self, value):
|
||||
# resolve variables
|
||||
if isinstance(value, string_types) and "$" in value:
|
||||
for k, v in self._state.items():
|
||||
for key_replace in ("${" + k + "}", "$" + k):
|
||||
if value == key_replace:
|
||||
value = v
|
||||
break
|
||||
# We only do the in-string replacement if using ${...}
|
||||
elif (
|
||||
key_replace.startswith("${")
|
||||
and isinstance(value, string_types)
|
||||
and key_replace in value
|
||||
):
|
||||
value = value.replace(key_replace, v)
|
||||
break
|
||||
|
||||
if isinstance(value, string_types):
|
||||
value = value.strip()
|
||||
elif isinstance(value, dict):
|
||||
value = dict((k, self._resolve(v)) for (k, v) in value.items())
|
||||
elif isinstance(value, list):
|
||||
value = list(map(self._resolve, value))
|
||||
return value
|
||||
|
||||
def _lookup(self, path):
|
||||
# fetch the possibly nested value from last_response
|
||||
value = self.last_response
|
||||
if path == "$body":
|
||||
return value
|
||||
path = path.replace(r"\.", "\1")
|
||||
for step in path.split("."):
|
||||
if not step:
|
||||
continue
|
||||
step = step.replace("\1", ".")
|
||||
step = self._resolve(step)
|
||||
|
||||
if (
|
||||
isinstance(step, string_types)
|
||||
and step.isdigit()
|
||||
and isinstance(value, list)
|
||||
):
|
||||
step = int(step)
|
||||
assert isinstance(value, list)
|
||||
assert len(value) > step
|
||||
elif step == "_arbitrary_key_":
|
||||
return list(value.keys())[0]
|
||||
else:
|
||||
assert step in value
|
||||
value = value[step]
|
||||
return value
|
||||
|
||||
def _feature_enabled(self, name):
|
||||
return False
|
||||
|
||||
def _assert_match_equals(self, a, b):
|
||||
# Handle for large floating points with 'E'
|
||||
if isinstance(b, string_types) and isinstance(a, float) and "e" in repr(a):
|
||||
a = repr(a).replace("e+", "E")
|
||||
|
||||
assert a == b, "%r does not match %r" % (a, b)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def sync_runner(sync_client):
|
||||
return YamlRunner(sync_client)
|
||||
|
||||
|
||||
YAML_TEST_SPECS = []
|
||||
|
||||
# Try loading the REST API test specs from the Elastic Artifacts API
|
||||
try:
|
||||
# Construct the HTTP and OpenSearch client
|
||||
http = urllib3.PoolManager(retries=10)
|
||||
client = get_client()
|
||||
|
||||
# Make a request to OpenSearch for the build hash, we'll be looking for
|
||||
# an artifact with this same hash to download test specs for.
|
||||
client_info = client.info()
|
||||
version_number = client_info["version"]["number"]
|
||||
build_hash = client_info["version"]["build_hash"]
|
||||
|
||||
# Now talk to the artifacts API with the 'STACK_VERSION' environment variable
|
||||
resp = http.request(
|
||||
"GET",
|
||||
"https://artifacts-api.elastic.co/v1/versions/%s" % (version_number,),
|
||||
)
|
||||
resp = json.loads(resp.data.decode("utf-8"))
|
||||
|
||||
# Look through every build and see if one matches the commit hash
|
||||
# we're looking for. If not it's okay, we'll just use the latest and
|
||||
# hope for the best!
|
||||
builds = resp["version"]["builds"]
|
||||
for build in builds:
|
||||
if build["projects"]["opensearch"]["commit_hash"] == build_hash:
|
||||
break
|
||||
else:
|
||||
build = builds[0] # Use the latest
|
||||
|
||||
# Now we're looking for the 'rest-api-spec-<VERSION>-sources.jar' file
|
||||
# to download and extract in-memory.
|
||||
packages = build["projects"]["opensearch"]["packages"]
|
||||
for package in packages:
|
||||
if re.match(r"rest-resources-zip-.*\.zip", package):
|
||||
package_url = packages[package]["url"]
|
||||
break
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"Could not find the package 'rest-resources-zip-*.zip' in build %r" % build
|
||||
)
|
||||
|
||||
# Download the zip and start reading YAML from the files in memory
|
||||
package_zip = zipfile.ZipFile(io.BytesIO(http.request("GET", package_url).data))
|
||||
for yaml_file in package_zip.namelist():
|
||||
if not re.match(r"^rest-api-spec/test/.*\.ya?ml$", yaml_file):
|
||||
continue
|
||||
yaml_tests = list(yaml.safe_load_all(package_zip.read(yaml_file)))
|
||||
|
||||
# Each file may have a "test" named 'setup' or 'teardown',
|
||||
# these sets of steps should be run at the beginning and end
|
||||
# of every other test within the file so we do one pass to capture those.
|
||||
setup_steps = teardown_steps = None
|
||||
test_numbers_and_steps = []
|
||||
test_number = 0
|
||||
|
||||
for yaml_test in yaml_tests:
|
||||
test_name, test_step = yaml_test.popitem()
|
||||
if test_name == "setup":
|
||||
setup_steps = test_step
|
||||
elif test_name == "teardown":
|
||||
teardown_steps = test_step
|
||||
else:
|
||||
test_numbers_and_steps.append((test_number, test_step))
|
||||
test_number += 1
|
||||
|
||||
# Now we combine setup, teardown, and test_steps into
|
||||
# a set of pytest.param() instances
|
||||
for test_number, test_step in test_numbers_and_steps:
|
||||
# Build the id from the name of the YAML file and
|
||||
# the number within that file. Most important step
|
||||
# is to remove most of the file path prefixes and
|
||||
# the .yml suffix.
|
||||
pytest_test_name = yaml_file.rpartition(".")[0].replace(".", "/")
|
||||
for prefix in ("rest-api-spec/", "test/", "oss/"):
|
||||
if pytest_test_name.startswith(prefix):
|
||||
pytest_test_name = pytest_test_name[len(prefix) :]
|
||||
pytest_param_id = "%s[%d]" % (pytest_test_name, test_number)
|
||||
|
||||
pytest_param = {
|
||||
"setup": setup_steps,
|
||||
"run": test_step,
|
||||
"teardown": teardown_steps,
|
||||
}
|
||||
# Skip either 'test_name' or 'test_name[x]'
|
||||
if pytest_test_name in SKIP_TESTS or pytest_param_id in SKIP_TESTS:
|
||||
pytest_param["skip"] = True
|
||||
|
||||
YAML_TEST_SPECS.append(pytest.param(pytest_param, id=pytest_param_id))
|
||||
|
||||
except Exception as e:
|
||||
warnings.warn("Could not load REST API tests: %s" % (str(e),))
|
||||
|
||||
|
||||
if not RUN_ASYNC_REST_API_TESTS:
|
||||
|
||||
@pytest.mark.parametrize("test_spec", YAML_TEST_SPECS)
|
||||
def test_rest_api_spec(test_spec, sync_runner):
|
||||
if test_spec.get("skip", False):
|
||||
pytest.skip("Manually skipped in 'SKIP_TESTS'")
|
||||
sync_runner.use_spec(test_spec)
|
||||
sync_runner.run()
|
||||
@@ -0,0 +1,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.
|
||||
@@ -0,0 +1,30 @@
|
||||
# -*- 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.
|
||||
|
||||
from unittest import TestCase
|
||||
|
||||
from opensearchpy import OpenSearch
|
||||
from opensearchpy.helpers.test import OPENSEARCH_URL
|
||||
|
||||
|
||||
class TestSecurity(TestCase):
|
||||
def test_security(self):
|
||||
client = OpenSearch(
|
||||
OPENSEARCH_URL,
|
||||
http_auth=("admin", "admin"),
|
||||
verify_certs=False,
|
||||
)
|
||||
|
||||
info = client.info()
|
||||
self.assertNotEqual(info["version"]["number"], "")
|
||||
self.assertNotEqual(info["tagline"], "")
|
||||
self.assertTrue(
|
||||
"build_flavor" in info["version"] or "distribution" in info["version"]
|
||||
)
|
||||
@@ -0,0 +1,407 @@
|
||||
# -*- 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.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
import time
|
||||
|
||||
from mock import patch
|
||||
|
||||
from opensearchpy.connection import Connection
|
||||
from opensearchpy.connection_pool import DummyConnectionPool
|
||||
from opensearchpy.exceptions import ConnectionError, TransportError
|
||||
from opensearchpy.transport import Transport, get_host_info
|
||||
|
||||
from .test_cases import TestCase
|
||||
|
||||
|
||||
class DummyConnection(Connection):
|
||||
def __init__(self, **kwargs):
|
||||
self.exception = kwargs.pop("exception", None)
|
||||
self.status, self.data = kwargs.pop("status", 200), kwargs.pop("data", "{}")
|
||||
self.headers = kwargs.pop("headers", {})
|
||||
self.calls = []
|
||||
super(DummyConnection, self).__init__(**kwargs)
|
||||
|
||||
def perform_request(self, *args, **kwargs):
|
||||
self.calls.append((args, kwargs))
|
||||
if self.exception:
|
||||
raise self.exception
|
||||
return self.status, self.headers, self.data
|
||||
|
||||
|
||||
CLUSTER_NODES = """{
|
||||
"_nodes" : {
|
||||
"total" : 1,
|
||||
"successful" : 1,
|
||||
"failed" : 0
|
||||
},
|
||||
"cluster_name" : "opensearch",
|
||||
"nodes" : {
|
||||
"SRZpKFZdQguhhvifmN6UVA" : {
|
||||
"name" : "SRZpKFZ",
|
||||
"transport_address" : "127.0.0.1:9300",
|
||||
"host" : "127.0.0.1",
|
||||
"ip" : "127.0.0.1",
|
||||
"version" : "5.0.0",
|
||||
"build_hash" : "253032b",
|
||||
"roles" : [ "master", "data", "ingest" ],
|
||||
"http" : {
|
||||
"bound_address" : [ "[fe80::1]:9200", "[::1]:9200", "127.0.0.1:9200" ],
|
||||
"publish_address" : "1.1.1.1:123",
|
||||
"max_content_length_in_bytes" : 104857600
|
||||
}
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
CLUSTER_NODES_7x_PUBLISH_HOST = """{
|
||||
"_nodes" : {
|
||||
"total" : 1,
|
||||
"successful" : 1,
|
||||
"failed" : 0
|
||||
},
|
||||
"cluster_name" : "opensearch",
|
||||
"nodes" : {
|
||||
"SRZpKFZdQguhhvifmN6UVA" : {
|
||||
"name" : "SRZpKFZ",
|
||||
"transport_address" : "127.0.0.1:9300",
|
||||
"host" : "127.0.0.1",
|
||||
"ip" : "127.0.0.1",
|
||||
"version" : "5.0.0",
|
||||
"build_hash" : "253032b",
|
||||
"roles" : [ "master", "data", "ingest" ],
|
||||
"http" : {
|
||||
"bound_address" : [ "[fe80::1]:9200", "[::1]:9200", "127.0.0.1:9200" ],
|
||||
"publish_address" : "somehost.tld/1.1.1.1:123",
|
||||
"max_content_length_in_bytes" : 104857600
|
||||
}
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
|
||||
class TestHostsInfoCallback(TestCase):
|
||||
def test_master_only_nodes_are_ignored(self):
|
||||
nodes = [
|
||||
{"roles": ["master"]},
|
||||
{"roles": ["master", "data", "ingest"]},
|
||||
{"roles": ["data", "ingest"]},
|
||||
{"roles": []},
|
||||
{},
|
||||
]
|
||||
chosen = [
|
||||
i
|
||||
for i, node_info in enumerate(nodes)
|
||||
if get_host_info(node_info, i) is not None
|
||||
]
|
||||
self.assertEqual([1, 2, 3, 4], chosen)
|
||||
|
||||
|
||||
class TestTransport(TestCase):
|
||||
def test_single_connection_uses_dummy_connection_pool(self):
|
||||
t = Transport([{}])
|
||||
self.assertIsInstance(t.connection_pool, DummyConnectionPool)
|
||||
t = Transport([{"host": "localhost"}])
|
||||
self.assertIsInstance(t.connection_pool, DummyConnectionPool)
|
||||
|
||||
def test_request_timeout_extracted_from_params_and_passed(self):
|
||||
t = Transport([{}], connection_class=DummyConnection)
|
||||
|
||||
t.perform_request("GET", "/", params={"request_timeout": 42})
|
||||
self.assertEqual(1, len(t.get_connection().calls))
|
||||
self.assertEqual(("GET", "/", {}, None), t.get_connection().calls[0][0])
|
||||
self.assertEqual(
|
||||
{"timeout": 42, "ignore": (), "headers": None},
|
||||
t.get_connection().calls[0][1],
|
||||
)
|
||||
|
||||
def test_opaque_id(self):
|
||||
t = Transport([{}], opaque_id="app-1", connection_class=DummyConnection)
|
||||
|
||||
t.perform_request("GET", "/")
|
||||
self.assertEqual(1, len(t.get_connection().calls))
|
||||
self.assertEqual(("GET", "/", None, None), t.get_connection().calls[0][0])
|
||||
self.assertEqual(
|
||||
{"timeout": None, "ignore": (), "headers": None},
|
||||
t.get_connection().calls[0][1],
|
||||
)
|
||||
|
||||
# Now try with an 'x-opaque-id' set on perform_request().
|
||||
t.perform_request("GET", "/", headers={"x-opaque-id": "request-1"})
|
||||
self.assertEqual(2, len(t.get_connection().calls))
|
||||
self.assertEqual(("GET", "/", None, None), t.get_connection().calls[1][0])
|
||||
self.assertEqual(
|
||||
{"timeout": None, "ignore": (), "headers": {"x-opaque-id": "request-1"}},
|
||||
t.get_connection().calls[1][1],
|
||||
)
|
||||
|
||||
def test_request_with_custom_user_agent_header(self):
|
||||
t = Transport([{}], connection_class=DummyConnection)
|
||||
|
||||
t.perform_request("GET", "/", headers={"user-agent": "my-custom-value/1.2.3"})
|
||||
self.assertEqual(1, len(t.get_connection().calls))
|
||||
self.assertEqual(
|
||||
{
|
||||
"timeout": None,
|
||||
"ignore": (),
|
||||
"headers": {"user-agent": "my-custom-value/1.2.3"},
|
||||
},
|
||||
t.get_connection().calls[0][1],
|
||||
)
|
||||
|
||||
def test_send_get_body_as_source(self):
|
||||
t = Transport([{}], send_get_body_as="source", connection_class=DummyConnection)
|
||||
|
||||
t.perform_request("GET", "/", body={})
|
||||
self.assertEqual(1, len(t.get_connection().calls))
|
||||
self.assertEqual(
|
||||
("GET", "/", {"source": "{}"}, None), t.get_connection().calls[0][0]
|
||||
)
|
||||
|
||||
def test_send_get_body_as_post(self):
|
||||
t = Transport([{}], send_get_body_as="POST", connection_class=DummyConnection)
|
||||
|
||||
t.perform_request("GET", "/", body={})
|
||||
self.assertEqual(1, len(t.get_connection().calls))
|
||||
self.assertEqual(("POST", "/", None, b"{}"), t.get_connection().calls[0][0])
|
||||
|
||||
def test_body_gets_encoded_into_bytes(self):
|
||||
t = Transport([{}], connection_class=DummyConnection)
|
||||
|
||||
t.perform_request("GET", "/", body="你好")
|
||||
self.assertEqual(1, len(t.get_connection().calls))
|
||||
self.assertEqual(
|
||||
("GET", "/", None, b"\xe4\xbd\xa0\xe5\xa5\xbd"),
|
||||
t.get_connection().calls[0][0],
|
||||
)
|
||||
|
||||
def test_body_bytes_get_passed_untouched(self):
|
||||
t = Transport([{}], connection_class=DummyConnection)
|
||||
|
||||
body = b"\xe4\xbd\xa0\xe5\xa5\xbd"
|
||||
t.perform_request("GET", "/", body=body)
|
||||
self.assertEqual(1, len(t.get_connection().calls))
|
||||
self.assertEqual(("GET", "/", None, body), t.get_connection().calls[0][0])
|
||||
|
||||
def test_body_surrogates_replaced_encoded_into_bytes(self):
|
||||
t = Transport([{}], connection_class=DummyConnection)
|
||||
|
||||
t.perform_request("GET", "/", body="你好\uda6a")
|
||||
self.assertEqual(1, len(t.get_connection().calls))
|
||||
self.assertEqual(
|
||||
("GET", "/", None, b"\xe4\xbd\xa0\xe5\xa5\xbd\xed\xa9\xaa"),
|
||||
t.get_connection().calls[0][0],
|
||||
)
|
||||
|
||||
def test_kwargs_passed_on_to_connections(self):
|
||||
t = Transport([{"host": "google.com"}], port=123)
|
||||
self.assertEqual(1, len(t.connection_pool.connections))
|
||||
self.assertEqual("http://google.com:123", t.connection_pool.connections[0].host)
|
||||
|
||||
def test_kwargs_passed_on_to_connection_pool(self):
|
||||
dt = object()
|
||||
t = Transport([{}, {}], dead_timeout=dt)
|
||||
self.assertIs(dt, t.connection_pool.dead_timeout)
|
||||
|
||||
def test_custom_connection_class(self):
|
||||
class MyConnection(object):
|
||||
def __init__(self, **kwargs):
|
||||
self.kwargs = kwargs
|
||||
|
||||
t = Transport([{}], connection_class=MyConnection)
|
||||
self.assertEqual(1, len(t.connection_pool.connections))
|
||||
self.assertIsInstance(t.connection_pool.connections[0], MyConnection)
|
||||
|
||||
def test_add_connection(self):
|
||||
t = Transport([{}], randomize_hosts=False)
|
||||
t.add_connection({"host": "google.com", "port": 1234})
|
||||
|
||||
self.assertEqual(2, len(t.connection_pool.connections))
|
||||
self.assertEqual(
|
||||
"http://google.com:1234", t.connection_pool.connections[1].host
|
||||
)
|
||||
|
||||
def test_request_will_fail_after_X_retries(self):
|
||||
t = Transport(
|
||||
[{"exception": ConnectionError("abandon ship")}],
|
||||
connection_class=DummyConnection,
|
||||
)
|
||||
|
||||
self.assertRaises(ConnectionError, t.perform_request, "GET", "/")
|
||||
self.assertEqual(4, len(t.get_connection().calls))
|
||||
|
||||
def test_failed_connection_will_be_marked_as_dead(self):
|
||||
t = Transport(
|
||||
[{"exception": ConnectionError("abandon ship")}] * 2,
|
||||
connection_class=DummyConnection,
|
||||
)
|
||||
|
||||
self.assertRaises(ConnectionError, t.perform_request, "GET", "/")
|
||||
self.assertEqual(0, len(t.connection_pool.connections))
|
||||
|
||||
def test_resurrected_connection_will_be_marked_as_live_on_success(self):
|
||||
for method in ("GET", "HEAD"):
|
||||
t = Transport([{}, {}], connection_class=DummyConnection)
|
||||
con1 = t.connection_pool.get_connection()
|
||||
con2 = t.connection_pool.get_connection()
|
||||
t.connection_pool.mark_dead(con1)
|
||||
t.connection_pool.mark_dead(con2)
|
||||
|
||||
t.perform_request(method, "/")
|
||||
self.assertEqual(1, len(t.connection_pool.connections))
|
||||
self.assertEqual(1, len(t.connection_pool.dead_count))
|
||||
|
||||
def test_sniff_will_use_seed_connections(self):
|
||||
t = Transport([{"data": CLUSTER_NODES}], connection_class=DummyConnection)
|
||||
t.set_connections([{"data": "invalid"}])
|
||||
|
||||
t.sniff_hosts()
|
||||
self.assertEqual(1, len(t.connection_pool.connections))
|
||||
self.assertEqual("http://1.1.1.1:123", t.get_connection().host)
|
||||
|
||||
def test_sniff_on_start_fetches_and_uses_nodes_list(self):
|
||||
t = Transport(
|
||||
[{"data": CLUSTER_NODES}],
|
||||
connection_class=DummyConnection,
|
||||
sniff_on_start=True,
|
||||
)
|
||||
self.assertEqual(1, len(t.connection_pool.connections))
|
||||
self.assertEqual("http://1.1.1.1:123", t.get_connection().host)
|
||||
|
||||
def test_sniff_on_start_ignores_sniff_timeout(self):
|
||||
t = Transport(
|
||||
[{"data": CLUSTER_NODES}],
|
||||
connection_class=DummyConnection,
|
||||
sniff_on_start=True,
|
||||
sniff_timeout=12,
|
||||
)
|
||||
self.assertEqual(
|
||||
(("GET", "/_nodes/_all/http"), {"timeout": None}),
|
||||
t.seed_connections[0].calls[0],
|
||||
)
|
||||
|
||||
def test_sniff_uses_sniff_timeout(self):
|
||||
t = Transport(
|
||||
[{"data": CLUSTER_NODES}],
|
||||
connection_class=DummyConnection,
|
||||
sniff_timeout=42,
|
||||
)
|
||||
t.sniff_hosts()
|
||||
self.assertEqual(
|
||||
(("GET", "/_nodes/_all/http"), {"timeout": 42}),
|
||||
t.seed_connections[0].calls[0],
|
||||
)
|
||||
|
||||
def test_sniff_reuses_connection_instances_if_possible(self):
|
||||
t = Transport(
|
||||
[{"data": CLUSTER_NODES}, {"host": "1.1.1.1", "port": 123}],
|
||||
connection_class=DummyConnection,
|
||||
randomize_hosts=False,
|
||||
)
|
||||
connection = t.connection_pool.connections[1]
|
||||
|
||||
t.sniff_hosts()
|
||||
self.assertEqual(1, len(t.connection_pool.connections))
|
||||
self.assertIs(connection, t.get_connection())
|
||||
|
||||
def test_sniff_on_fail_triggers_sniffing_on_fail(self):
|
||||
t = Transport(
|
||||
[{"exception": ConnectionError("abandon ship")}, {"data": CLUSTER_NODES}],
|
||||
connection_class=DummyConnection,
|
||||
sniff_on_connection_fail=True,
|
||||
max_retries=0,
|
||||
randomize_hosts=False,
|
||||
)
|
||||
|
||||
self.assertRaises(ConnectionError, t.perform_request, "GET", "/")
|
||||
self.assertEqual(1, len(t.connection_pool.connections))
|
||||
self.assertEqual("http://1.1.1.1:123", t.get_connection().host)
|
||||
|
||||
@patch("opensearchpy.transport.Transport.sniff_hosts")
|
||||
def test_sniff_on_fail_failing_does_not_prevent_retires(self, sniff_hosts):
|
||||
sniff_hosts.side_effect = [TransportError("sniff failed")]
|
||||
t = Transport(
|
||||
[{"exception": ConnectionError("abandon ship")}, {"data": CLUSTER_NODES}],
|
||||
connection_class=DummyConnection,
|
||||
sniff_on_connection_fail=True,
|
||||
max_retries=3,
|
||||
randomize_hosts=False,
|
||||
)
|
||||
|
||||
conn_err, conn_data = t.connection_pool.connections
|
||||
response = t.perform_request("GET", "/")
|
||||
self.assertEqual(json.loads(CLUSTER_NODES), response)
|
||||
self.assertEqual(1, sniff_hosts.call_count)
|
||||
self.assertEqual(1, len(conn_err.calls))
|
||||
self.assertEqual(1, len(conn_data.calls))
|
||||
|
||||
def test_sniff_after_n_seconds(self):
|
||||
t = Transport(
|
||||
[{"data": CLUSTER_NODES}],
|
||||
connection_class=DummyConnection,
|
||||
sniffer_timeout=5,
|
||||
)
|
||||
|
||||
for _ in range(4):
|
||||
t.perform_request("GET", "/")
|
||||
self.assertEqual(1, len(t.connection_pool.connections))
|
||||
self.assertIsInstance(t.get_connection(), DummyConnection)
|
||||
t.last_sniff = time.time() - 5.1
|
||||
|
||||
t.perform_request("GET", "/")
|
||||
self.assertEqual(1, len(t.connection_pool.connections))
|
||||
self.assertEqual("http://1.1.1.1:123", t.get_connection().host)
|
||||
self.assertTrue(time.time() - 1 < t.last_sniff < time.time() + 0.01)
|
||||
|
||||
def test_sniff_7x_publish_host(self):
|
||||
# Test the response shaped when a 7.x node has publish_host set
|
||||
# and the returend data is shaped in the fqdn/ip:port format.
|
||||
t = Transport(
|
||||
[{"data": CLUSTER_NODES_7x_PUBLISH_HOST}],
|
||||
connection_class=DummyConnection,
|
||||
sniff_timeout=42,
|
||||
)
|
||||
t.sniff_hosts()
|
||||
# Ensure we parsed out the fqdn and port from the fqdn/ip:port string.
|
||||
self.assertEqual(
|
||||
t.connection_pool.connection_opts[0][1],
|
||||
{"host": "somehost.tld", "port": 123},
|
||||
)
|
||||
|
||||
@patch("opensearchpy.transport.Transport.sniff_hosts")
|
||||
def test_sniffing_disabled_on_cloud_instances(self, sniff_hosts):
|
||||
t = Transport(
|
||||
[{}],
|
||||
sniff_on_start=True,
|
||||
sniff_on_connection_fail=True,
|
||||
cloud_id="cluster:dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyQ0ZmE4ODIxZTc1NjM0MDMyYmVkMWNmMjIxMTBlMmY5NyQ0ZmE4ODIxZTc1NjM0MDMyYmVkMWNmMjIxMTBlMmY5Ng==",
|
||||
)
|
||||
|
||||
self.assertFalse(t.sniff_on_connection_fail)
|
||||
self.assertIs(sniff_hosts.call_args, None) # Assert not called.
|
||||
@@ -0,0 +1,6 @@
|
||||
# Type Hints
|
||||
|
||||
All of these scripts are used to test the type hinting
|
||||
distributed with the `opensearch` package.
|
||||
These scripts simulate normal usage of the client and are run
|
||||
through `mypy --strict` as a part of continuous integration.
|
||||
@@ -0,0 +1,210 @@
|
||||
# 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.
|
||||
|
||||
from typing import Any, AsyncGenerator, Dict, Generator
|
||||
|
||||
from opensearchpy1 import (
|
||||
AIOHttpConnection,
|
||||
AsyncOpenSearch,
|
||||
AsyncTransport,
|
||||
ConnectionPool,
|
||||
OpenSearch,
|
||||
RequestsHttpConnection,
|
||||
Transport,
|
||||
)
|
||||
from opensearchpy1.helpers import (
|
||||
async_bulk,
|
||||
async_reindex,
|
||||
async_scan,
|
||||
async_streaming_bulk,
|
||||
bulk,
|
||||
reindex,
|
||||
scan,
|
||||
streaming_bulk,
|
||||
)
|
||||
|
||||
client = OpenSearch(
|
||||
[{"host": "localhost", "port": 9443}],
|
||||
transport_class=Transport,
|
||||
)
|
||||
t = Transport(
|
||||
[{}],
|
||||
connection_class=RequestsHttpConnection,
|
||||
connection_pool_class=ConnectionPool,
|
||||
sniff_on_start=True,
|
||||
sniffer_timeout=0.1,
|
||||
sniff_timeout=1,
|
||||
sniff_on_connection_fail=False,
|
||||
max_retries=1,
|
||||
retry_on_status={100, 400, 503},
|
||||
retry_on_timeout=True,
|
||||
send_get_body_as="source",
|
||||
)
|
||||
|
||||
|
||||
def sync_gen() -> Generator[Dict[Any, Any], None, None]:
|
||||
yield {}
|
||||
|
||||
|
||||
def scan_types() -> None:
|
||||
for _ in scan(
|
||||
client,
|
||||
query={"query": {"match_all": {}}},
|
||||
request_timeout=10,
|
||||
clear_scroll=True,
|
||||
scroll_kwargs={"request_timeout": 10},
|
||||
):
|
||||
pass
|
||||
for _ in scan(
|
||||
client,
|
||||
raise_on_error=False,
|
||||
preserve_order=False,
|
||||
scroll="10m",
|
||||
size=10,
|
||||
request_timeout=10.0,
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
def streaming_bulk_types() -> None:
|
||||
for _ in streaming_bulk(client, sync_gen()):
|
||||
pass
|
||||
for _ in streaming_bulk(client, sync_gen().__iter__()):
|
||||
pass
|
||||
for _ in streaming_bulk(client, [{}]):
|
||||
pass
|
||||
for _ in streaming_bulk(client, ({},)):
|
||||
pass
|
||||
|
||||
|
||||
def bulk_types() -> None:
|
||||
_, _ = bulk(client, sync_gen())
|
||||
_, _ = bulk(client, sync_gen().__iter__())
|
||||
_, _ = bulk(client, [{}])
|
||||
_, _ = bulk(client, ({},))
|
||||
|
||||
|
||||
def reindex_types() -> None:
|
||||
_, _ = reindex(
|
||||
client, "src-index", "target-index", query={"query": {"match": {"key": "val"}}}
|
||||
)
|
||||
_, _ = reindex(
|
||||
client,
|
||||
source_index="src-index",
|
||||
target_index="target-index",
|
||||
target_client=client,
|
||||
)
|
||||
_, _ = reindex(
|
||||
client,
|
||||
"src-index",
|
||||
"target-index",
|
||||
chunk_size=1,
|
||||
scroll="10m",
|
||||
scan_kwargs={"request_timeout": 10},
|
||||
bulk_kwargs={"request_timeout": 10},
|
||||
)
|
||||
|
||||
|
||||
client2 = AsyncOpenSearch(
|
||||
[{"host": "localhost", "port": 9443}],
|
||||
transport_class=AsyncTransport,
|
||||
)
|
||||
t2 = AsyncTransport(
|
||||
[{}],
|
||||
connection_class=AIOHttpConnection,
|
||||
connection_pool_class=ConnectionPool,
|
||||
sniff_on_start=True,
|
||||
sniffer_timeout=0.1,
|
||||
sniff_timeout=1,
|
||||
sniff_on_connection_fail=False,
|
||||
max_retries=1,
|
||||
retry_on_status={100, 400, 503},
|
||||
retry_on_timeout=True,
|
||||
send_get_body_as="source",
|
||||
)
|
||||
|
||||
|
||||
async def async_gen() -> AsyncGenerator[Dict[Any, Any], None]:
|
||||
yield {}
|
||||
|
||||
|
||||
async def async_scan_types() -> None:
|
||||
async for _ in async_scan(
|
||||
client2,
|
||||
query={"query": {"match_all": {}}},
|
||||
request_timeout=10,
|
||||
clear_scroll=True,
|
||||
scroll_kwargs={"request_timeout": 10},
|
||||
):
|
||||
pass
|
||||
async for _ in async_scan(
|
||||
client2,
|
||||
raise_on_error=False,
|
||||
preserve_order=False,
|
||||
scroll="10m",
|
||||
size=10,
|
||||
request_timeout=10.0,
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
async def async_streaming_bulk_types() -> None:
|
||||
async for _ in async_streaming_bulk(client2, async_gen()):
|
||||
pass
|
||||
async for _ in async_streaming_bulk(client2, async_gen().__aiter__()):
|
||||
pass
|
||||
async for _ in async_streaming_bulk(client2, [{}]):
|
||||
pass
|
||||
async for _ in async_streaming_bulk(client2, ({},)):
|
||||
pass
|
||||
|
||||
|
||||
async def async_bulk_types() -> None:
|
||||
_, _ = await async_bulk(client2, async_gen())
|
||||
_, _ = await async_bulk(client2, async_gen().__aiter__())
|
||||
_, _ = await async_bulk(client2, [{}])
|
||||
_, _ = await async_bulk(client2, ({},))
|
||||
|
||||
|
||||
async def async_reindex_types() -> None:
|
||||
_, _ = await async_reindex(
|
||||
client2, "src-index", "target-index", query={"query": {"match": {"key": "val"}}}
|
||||
)
|
||||
_, _ = await async_reindex(
|
||||
client2,
|
||||
source_index="src-index",
|
||||
target_index="target-index",
|
||||
target_client=client2,
|
||||
)
|
||||
_, _ = await async_reindex(
|
||||
client2,
|
||||
"src-index",
|
||||
"target-index",
|
||||
chunk_size=1,
|
||||
scroll="10m",
|
||||
scan_kwargs={"request_timeout": 10},
|
||||
bulk_kwargs={"request_timeout": 10},
|
||||
)
|
||||
@@ -0,0 +1,121 @@
|
||||
# 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.
|
||||
|
||||
from typing import Any, AsyncGenerator, Dict
|
||||
|
||||
from opensearchpy import (
|
||||
AIOHttpConnection,
|
||||
AsyncOpenSearch,
|
||||
AsyncTransport,
|
||||
ConnectionPool,
|
||||
)
|
||||
from opensearchpy.helpers import (
|
||||
async_bulk,
|
||||
async_reindex,
|
||||
async_scan,
|
||||
async_streaming_bulk,
|
||||
)
|
||||
|
||||
client = AsyncOpenSearch(
|
||||
[{"host": "localhost", "port": 9443}],
|
||||
transport_class=AsyncTransport,
|
||||
)
|
||||
t = AsyncTransport(
|
||||
[{}],
|
||||
connection_class=AIOHttpConnection,
|
||||
connection_pool_class=ConnectionPool,
|
||||
sniff_on_start=True,
|
||||
sniffer_timeout=0.1,
|
||||
sniff_timeout=1,
|
||||
sniff_on_connection_fail=False,
|
||||
max_retries=1,
|
||||
retry_on_status={100, 400, 503},
|
||||
retry_on_timeout=True,
|
||||
send_get_body_as="source",
|
||||
)
|
||||
|
||||
|
||||
async def async_gen() -> AsyncGenerator[Dict[Any, Any], None]:
|
||||
yield {}
|
||||
|
||||
|
||||
async def async_scan_types() -> None:
|
||||
async for _ in async_scan(
|
||||
client,
|
||||
query={"query": {"match_all": {}}},
|
||||
request_timeout=10,
|
||||
clear_scroll=True,
|
||||
scroll_kwargs={"request_timeout": 10},
|
||||
):
|
||||
pass
|
||||
async for _ in async_scan(
|
||||
client,
|
||||
raise_on_error=False,
|
||||
preserve_order=False,
|
||||
scroll="10m",
|
||||
size=10,
|
||||
request_timeout=10.0,
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
async def async_streaming_bulk_types() -> None:
|
||||
async for _ in async_streaming_bulk(client, async_gen()):
|
||||
pass
|
||||
async for _ in async_streaming_bulk(client, async_gen().__aiter__()):
|
||||
pass
|
||||
async for _ in async_streaming_bulk(client, [{}]):
|
||||
pass
|
||||
async for _ in async_streaming_bulk(client, ({},)):
|
||||
pass
|
||||
|
||||
|
||||
async def async_bulk_types() -> None:
|
||||
_, _ = await async_bulk(client, async_gen())
|
||||
_, _ = await async_bulk(client, async_gen().__aiter__())
|
||||
_, _ = await async_bulk(client, [{}])
|
||||
_, _ = await async_bulk(client, ({},))
|
||||
|
||||
|
||||
async def async_reindex_types() -> None:
|
||||
_, _ = await async_reindex(
|
||||
client, "src-index", "target-index", query={"query": {"match": {"key": "val"}}}
|
||||
)
|
||||
_, _ = await async_reindex(
|
||||
client,
|
||||
source_index="src-index",
|
||||
target_index="target-index",
|
||||
target_client=client,
|
||||
)
|
||||
_, _ = await async_reindex(
|
||||
client,
|
||||
"src-index",
|
||||
"target-index",
|
||||
chunk_size=1,
|
||||
scroll="10m",
|
||||
scan_kwargs={"request_timeout": 10},
|
||||
bulk_kwargs={"request_timeout": 10},
|
||||
)
|
||||
@@ -0,0 +1,111 @@
|
||||
# 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.
|
||||
|
||||
from typing import Any, Dict, Generator
|
||||
|
||||
from opensearchpy import ConnectionPool, OpenSearch, RequestsHttpConnection, Transport
|
||||
from opensearchpy.helpers import bulk, reindex, scan, streaming_bulk
|
||||
|
||||
client = OpenSearch(
|
||||
[{"host": "localhost", "port": 9443}],
|
||||
transport_class=Transport,
|
||||
)
|
||||
t = Transport(
|
||||
[{}],
|
||||
connection_class=RequestsHttpConnection,
|
||||
connection_pool_class=ConnectionPool,
|
||||
sniff_on_start=True,
|
||||
sniffer_timeout=0.1,
|
||||
sniff_timeout=1,
|
||||
sniff_on_connection_fail=False,
|
||||
max_retries=1,
|
||||
retry_on_status={100, 400, 503},
|
||||
retry_on_timeout=True,
|
||||
send_get_body_as="source",
|
||||
)
|
||||
|
||||
|
||||
def sync_gen() -> Generator[Dict[Any, Any], None, None]:
|
||||
yield {}
|
||||
|
||||
|
||||
def scan_types() -> None:
|
||||
for _ in scan(
|
||||
client,
|
||||
query={"query": {"match_all": {}}},
|
||||
request_timeout=10,
|
||||
clear_scroll=True,
|
||||
scroll_kwargs={"request_timeout": 10},
|
||||
):
|
||||
pass
|
||||
for _ in scan(
|
||||
client,
|
||||
raise_on_error=False,
|
||||
preserve_order=False,
|
||||
scroll="10m",
|
||||
size=10,
|
||||
request_timeout=10.0,
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
def streaming_bulk_types() -> None:
|
||||
for _ in streaming_bulk(client, sync_gen()):
|
||||
pass
|
||||
for _ in streaming_bulk(client, sync_gen().__iter__()):
|
||||
pass
|
||||
for _ in streaming_bulk(client, [{}]):
|
||||
pass
|
||||
for _ in streaming_bulk(client, ({},)):
|
||||
pass
|
||||
|
||||
|
||||
def bulk_types() -> None:
|
||||
_, _ = bulk(client, sync_gen())
|
||||
_, _ = bulk(client, sync_gen().__iter__())
|
||||
_, _ = bulk(client, [{}])
|
||||
_, _ = bulk(client, ({},))
|
||||
|
||||
|
||||
def reindex_types() -> None:
|
||||
_, _ = reindex(
|
||||
client, "src-index", "target-index", query={"query": {"match": {"key": "val"}}}
|
||||
)
|
||||
_, _ = reindex(
|
||||
client,
|
||||
source_index="src-index",
|
||||
target_index="target-index",
|
||||
target_client=client,
|
||||
)
|
||||
_, _ = reindex(
|
||||
client,
|
||||
"src-index",
|
||||
"target-index",
|
||||
chunk_size=1,
|
||||
scroll="10m",
|
||||
scan_kwargs={"request_timeout": 10},
|
||||
bulk_kwargs={"request_timeout": 10},
|
||||
)
|
||||
@@ -0,0 +1,176 @@
|
||||
# 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 time
|
||||
|
||||
from opensearchpy import OpenSearch
|
||||
|
||||
|
||||
def wipe_cluster(client):
|
||||
"""Wipes a cluster clean between test cases"""
|
||||
close_after_wipe = False
|
||||
try:
|
||||
# If client is async we need to replace the client
|
||||
# with a synchronous one.
|
||||
from opensearchpy import AsyncOpenSearch
|
||||
|
||||
if isinstance(client, AsyncOpenSearch):
|
||||
client = OpenSearch(client.transport.hosts, verify_certs=False)
|
||||
close_after_wipe = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
wipe_snapshots(client)
|
||||
wipe_indices(client)
|
||||
|
||||
client.indices.delete_template(name="*")
|
||||
client.indices.delete_index_template(name="*")
|
||||
client.cluster.delete_component_template(name="*")
|
||||
|
||||
wipe_cluster_settings(client)
|
||||
|
||||
wait_for_cluster_state_updates_to_finish(client)
|
||||
if close_after_wipe:
|
||||
client.close()
|
||||
|
||||
|
||||
def wipe_cluster_settings(client):
|
||||
settings = client.cluster.get_settings()
|
||||
new_settings = {}
|
||||
for name, value in settings.items():
|
||||
if value:
|
||||
new_settings.setdefault(name, {})
|
||||
for key in value.keys():
|
||||
new_settings[name][key + ".*"] = None
|
||||
if new_settings:
|
||||
client.cluster.put_settings(body=new_settings)
|
||||
|
||||
|
||||
def wipe_snapshots(client):
|
||||
"""Deletes all the snapshots and repositories from the cluster"""
|
||||
in_progress_snapshots = []
|
||||
|
||||
repos = client.snapshot.get_repository(repository="_all")
|
||||
for repo_name, repo in repos.items():
|
||||
if repo["type"] == "fs":
|
||||
snapshots = client.snapshot.get(
|
||||
repository=repo_name, snapshot="_all", ignore_unavailable=True
|
||||
)
|
||||
for snapshot in snapshots["snapshots"]:
|
||||
if snapshot["state"] == "IN_PROGRESS":
|
||||
in_progress_snapshots.append(snapshot)
|
||||
else:
|
||||
client.snapshot.delete(
|
||||
repository=repo_name,
|
||||
snapshot=snapshot["snapshot"],
|
||||
ignore=404,
|
||||
)
|
||||
|
||||
client.snapshot.delete_repository(repository=repo_name, ignore=404)
|
||||
|
||||
assert in_progress_snapshots == []
|
||||
|
||||
|
||||
def wipe_data_streams(client):
|
||||
try:
|
||||
client.indices.delete_data_stream(name="*", expand_wildcards="all")
|
||||
except Exception:
|
||||
client.indices.delete_data_stream(name="*")
|
||||
|
||||
|
||||
def wipe_indices(client):
|
||||
client.indices.delete(
|
||||
index="*,-.ds-ilm-history-*",
|
||||
expand_wildcards="all",
|
||||
ignore=404,
|
||||
)
|
||||
|
||||
|
||||
def wipe_searchable_snapshot_indices(client):
|
||||
cluster_metadata = client.cluster.state(
|
||||
metric="metadata",
|
||||
filter_path="metadata.indices.*.settings.index.store.snapshot",
|
||||
)
|
||||
if cluster_metadata:
|
||||
for index in cluster_metadata["metadata"]["indices"].keys():
|
||||
client.indices.delete(index=index)
|
||||
|
||||
|
||||
def wipe_slm_policies(client):
|
||||
for policy in client.slm.get_lifecycle():
|
||||
client.slm.delete_lifecycle(policy_id=policy["name"])
|
||||
|
||||
|
||||
def wipe_auto_follow_patterns(client):
|
||||
for pattern in client.ccr.get_auto_follow_pattern()["patterns"]:
|
||||
client.ccr.delete_auto_follow_pattern(name=pattern["name"])
|
||||
|
||||
|
||||
def wipe_node_shutdown_metadata(client):
|
||||
shutdown_status = client.shutdown.get_node()
|
||||
# If response contains these two keys the feature flag isn't enabled
|
||||
# on this cluster so skip this step now.
|
||||
if "_nodes" in shutdown_status and "cluster_name" in shutdown_status:
|
||||
return
|
||||
|
||||
for shutdown_node in shutdown_status.get("nodes", []):
|
||||
node_id = shutdown_node["node_id"]
|
||||
client.shutdown.delete_node(node_id=node_id)
|
||||
|
||||
|
||||
def wipe_tasks(client):
|
||||
tasks = client.tasks.list()
|
||||
for node_name, node in tasks.get("node", {}).items():
|
||||
for task_id in node.get("tasks", ()):
|
||||
client.tasks.cancel(task_id=task_id, wait_for_completion=True)
|
||||
|
||||
|
||||
def wait_for_pending_tasks(client, filter, timeout=30):
|
||||
end_time = time.time() + timeout
|
||||
while time.time() < end_time:
|
||||
tasks = client.cat.tasks(detailed=True).split("\n")
|
||||
if not any(filter in task for task in tasks):
|
||||
break
|
||||
|
||||
|
||||
def wait_for_pending_datafeeds_and_jobs(client, timeout=30):
|
||||
end_time = time.time() + timeout
|
||||
while time.time() < end_time:
|
||||
if (
|
||||
client.ml.get_datafeeds(datafeed_id="*", allow_no_datafeeds=True)["count"]
|
||||
== 0
|
||||
):
|
||||
break
|
||||
while time.time() < end_time:
|
||||
if client.ml.get_jobs(job_id="*", allow_no_jobs=True)["count"] == 0:
|
||||
break
|
||||
|
||||
|
||||
def wait_for_cluster_state_updates_to_finish(client, timeout=30):
|
||||
end_time = time.time() + timeout
|
||||
while time.time() < end_time:
|
||||
if not client.cluster.pending_tasks().get("tasks", ()):
|
||||
break
|
||||
Reference in New Issue
Block a user