├── MANIFEST.in ├── requirements.txt ├── NOTICE.txt ├── elastic_app_search ├── __init__.py ├── __version__.py ├── exceptions.py ├── request_session.py └── client.py ├── logo-app-search.png ├── tests ├── __init__.py ├── test_request_session.py └── test_client.py ├── setup.cfg ├── tox.ini ├── .circleci └── config.yml ├── setup.py ├── .gitignore ├── LICENSE.txt └── README.md /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.20.0 2 | PyJWT==1.5.3 3 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | Elastic App Search Python client. 2 | 3 | Copyright 2012-2019 Elasticsearch B.V. 4 | -------------------------------------------------------------------------------- /elastic_app_search/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import Client 2 | from .__version__ import __version__ 3 | -------------------------------------------------------------------------------- /logo-app-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elastic/app-search-python/HEAD/logo-app-search.png -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # the inclusion of the tests module is not meant to offer best practices for 2 | # testing in general, but rather to support the `find_packages` example in 3 | # setup.py that excludes installing the "tests" package 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | # This flag says that the code is written to work on both Python 2 and Python 3 | # 3. If at all possible, it is good practice to do this. If you cannot, you 4 | # will need to generate wheels for each Python version that you support. 5 | universal=1 6 | -------------------------------------------------------------------------------- /elastic_app_search/__version__.py: -------------------------------------------------------------------------------- 1 | __title__ = 'elastic-app-search' 2 | __description__ = 'An API client for Elastic App Search' 3 | __url__ = 'https://github.com/elastic/app-search-python' 4 | __version__ = '7.10.0' 5 | __author__ = 'Elastic' 6 | __author_email__ = 'support@elastic.co' 7 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # this file is *not* meant to cover or endorse the use of tox or pytest or 2 | # testing in general, 3 | # 4 | # It's meant to show the use of: 5 | # 6 | # - check-manifest 7 | # confirm items checked into vcs are in your sdist 8 | # - python setup.py check (using the readme_renderer extension) 9 | # confirms your long_description will render correctly on pypi 10 | # 11 | # and also to help confirm pull requests to this project. 12 | 13 | [tox] 14 | envlist = py{27,33,34,35,36} 15 | 16 | [testenv] 17 | basepython = 18 | py27: python2.7 19 | py33: python3.3 20 | py34: python3.4 21 | py35: python3.5 22 | py36: python3.6 23 | deps = 24 | check-manifest 25 | readme_renderer 26 | flake8 27 | pytest 28 | commands = 29 | check-manifest --ignore tox.ini,tests* 30 | python setup.py check -m -r -s 31 | flake8 . 32 | py.test tests 33 | [flake8] 34 | exclude = .tox,*.egg,build 35 | select = E,W,F 36 | -------------------------------------------------------------------------------- /elastic_app_search/exceptions.py: -------------------------------------------------------------------------------- 1 | """Exceptions raised by Elastic App Search Client.""" 2 | 3 | class ElasticAppSearchError(Exception): 4 | """Base class for all Elastic App Search errors.""" 5 | 6 | class InvalidCredentials(ElasticAppSearchError): 7 | """Raised when request cannot authenticate""" 8 | 9 | class NonExistentRecord(ElasticAppSearchError): 10 | """Raised when record does not exist""" 11 | 12 | class RecordAlreadyExists(ElasticAppSearchError): 13 | """Raised when record already exists""" 14 | 15 | class BadRequest(ElasticAppSearchError): 16 | """Raised when bad request""" 17 | 18 | def __init__(self, message): 19 | super(ElasticAppSearchError, self).__init__(message) 20 | 21 | class Forbidden(ElasticAppSearchError): 22 | """Raised when http forbidden""" 23 | 24 | class SynchronousDocumentIndexingFailed(ElasticAppSearchError): 25 | """Raised when synchronous indexing of documents takes too long""" 26 | 27 | class InvalidDocument(ElasticAppSearchError): 28 | """When a document has a non-accepted field or is missing a required field""" 29 | 30 | def __init__(self, message, document): 31 | super(ElasticAppSearchError, self).__init__(message) 32 | self.document = document 33 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2.1 3 | 4 | executors: 5 | python-27: { docker: [{ image: "python:2.7.16" }] } 6 | python-35: { docker: [{ image: "python:3.5.7" }] } 7 | python-36: { docker: [{ image: "python:3.6.9" }] } 8 | python-37: { docker: [{ image: "python:3.7.4" }] } 9 | 10 | jobs: 11 | build: 12 | parameters: 13 | executor: 14 | type: executor 15 | executor: << parameters.executor >> 16 | working_directory: ~/repo 17 | steps: 18 | - checkout 19 | - restore_cache: 20 | keys: 21 | - v1-dependencies-{{ .Environment.CIRCLE_JOB }}-{{ checksum "requirements.txt" }} 22 | - run: 23 | name: install dependencies 24 | command: | 25 | pip install virtualenv 26 | virtualenv venv 27 | . venv/bin/activate 28 | python setup.py install 29 | - save_cache: 30 | paths: 31 | - ./venv 32 | key: v1-dependencies-{{ .Environment.CIRCLE_JOB }}-{{ checksum "requirements.txt" }} 33 | - run: 34 | name: run tests 35 | command: | 36 | . venv/bin/activate 37 | python setup.py test 38 | 39 | workflows: 40 | run-tests: 41 | jobs: 42 | - build: { name: run-tests-python-2.7, executor: python-27 } 43 | - build: { name: run-tests-python-3.5, executor: python-35 } 44 | - build: { name: run-tests-python-3.6, executor: python-36 } 45 | - build: { name: run-tests-python-3.7, executor: python-37 } 46 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from codecs import open 3 | from os import path 4 | 5 | here = path.abspath(path.dirname(__file__)) 6 | 7 | with open(path.join(here, 'README.md'), encoding='utf-8') as f: 8 | long_description = f.read() 9 | 10 | here = path.abspath(path.dirname(__file__)) 11 | about = {} 12 | with open(path.join(here, 'elastic_app_search', '__version__.py'), 'r', 'utf-8') as f: 13 | exec(f.read(), about) 14 | 15 | setup( 16 | name=about['__title__'], 17 | version=about['__version__'], 18 | description=about['__description__'], 19 | long_description=long_description, 20 | long_description_content_type='text/markdown', 21 | url=about['__url__'], 22 | author=about['__author__'], 23 | author_email=about['__author_email__'], 24 | license='Apache 2.0', 25 | classifiers=[ 26 | 'Development Status :: 4 - Beta', 27 | 'Intended Audience :: Developers', 28 | 'License :: OSI Approved :: Apache Software License', 29 | 'Programming Language :: Python :: 2', 30 | 'Programming Language :: Python :: 2.7', 31 | 'Programming Language :: Python :: 3', 32 | 'Programming Language :: Python :: 3.3', 33 | 'Programming Language :: Python :: 3.4', 34 | 'Programming Language :: Python :: 3.5', 35 | 'Programming Language :: Python :: 3.6', 36 | ], 37 | keywords='elastic app search api', 38 | packages=find_packages(exclude=['contrib', 'docs', 'tests']), 39 | install_requires=[ 40 | 'requests', 41 | 'PyJWT<=1.7.1' 42 | ], 43 | tests_require=[ 44 | 'requests_mock', 45 | 'future' 46 | ], 47 | test_suite='tests' 48 | ) 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *.cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # Jupyter Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # SageMath parsed files 79 | *.sage.py 80 | 81 | # Environments 82 | .env 83 | .venv 84 | env/ 85 | venv/ 86 | ENV/ 87 | 88 | # Spyder project settings 89 | .spyderproject 90 | .spyproject 91 | 92 | # Rope project settings 93 | .ropeproject 94 | 95 | # mkdocs documentation 96 | /site 97 | 98 | # mypy 99 | .mypy_cache/ 100 | 101 | doc/_* 102 | .idea/ 103 | -------------------------------------------------------------------------------- /elastic_app_search/request_session.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import elastic_app_search 3 | from .exceptions import InvalidCredentials, NonExistentRecord, RecordAlreadyExists, BadRequest, Forbidden 4 | 5 | 6 | class RequestSession: 7 | 8 | def __init__(self, api_key, base_url): 9 | self.api_key = api_key 10 | self.base_url = base_url 11 | self.session = requests.Session() 12 | 13 | headers = { 14 | 'Authorization': "Bearer {}".format(api_key), 15 | 'X-Swiftype-Client': 'elastic-app-search-python', 16 | 'X-Swiftype-Client-Version': elastic_app_search.__version__, 17 | 'content-type': 'application/json; charset=utf8' 18 | } 19 | self.session.headers.update(headers) 20 | 21 | def raise_if_error(self, response): 22 | if response.status_code == requests.codes.unauthorized: 23 | raise InvalidCredentials(response.reason) 24 | elif response.status_code == requests.codes.bad: 25 | raise BadRequest(response.text) 26 | elif response.status_code == requests.codes.conflict: 27 | raise RecordAlreadyExists() 28 | elif response.status_code == requests.codes.not_found: 29 | raise NonExistentRecord() 30 | elif response.status_code == requests.codes.forbidden: 31 | raise Forbidden() 32 | 33 | response.raise_for_status() 34 | 35 | def request(self, http_method, endpoint, base_url=None, **kwargs): 36 | return self.request_ignore_response(http_method, endpoint, base_url, **kwargs).json() 37 | 38 | def request_ignore_response(self, http_method, endpoint, base_url=None, **kwargs): 39 | base_url = base_url or self.base_url 40 | url = "{}/{}".format(base_url, endpoint) 41 | response = self.session.request(http_method, url, **kwargs) 42 | self.raise_if_error(response) 43 | return response 44 | -------------------------------------------------------------------------------- /tests/test_request_session.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase, skipIf 2 | from requests.status_codes import codes 3 | from future.utils import iteritems 4 | import requests_mock 5 | 6 | import elastic_app_search 7 | from elastic_app_search.request_session import RequestSession 8 | from elastic_app_search.exceptions import InvalidCredentials 9 | 10 | 11 | class TestRequestSession(TestCase): 12 | 13 | api_host_key = 'api_host_key' 14 | 15 | def setUp(self): 16 | self.session = RequestSession(self.api_host_key, 'http://www.base_url.com') 17 | 18 | def test_request_success(self): 19 | expected_return = {'foo': 'bar'} 20 | endpoint = 'some_endpoint' 21 | 22 | with requests_mock.Mocker() as m: 23 | m.register_uri('POST', "{}/{}".format(self.session.base_url, endpoint), json=expected_return, status_code=200) 24 | response = self.session.request('post', endpoint) 25 | self.assertEqual(response, expected_return) 26 | 27 | def test_headers_initialization(self): 28 | headers_to_check = { 29 | k: v 30 | for k, v in iteritems(self.session.session.headers) 31 | if k in ['Authorization', 'X-Swiftype-Client', 'X-Swiftype-Client-Version'] 32 | } 33 | version = elastic_app_search.__version__ 34 | self.assertEqual( 35 | headers_to_check, 36 | { 37 | 'Authorization': 'Bearer {}'.format(self.api_host_key), 38 | 'X-Swiftype-Client': 'elastic-app-search-python', 39 | 'X-Swiftype-Client-Version': version 40 | } 41 | ) 42 | 43 | def test_request_throw_error(self): 44 | endpoint = 'some_endpoint' 45 | 46 | with requests_mock.Mocker() as m: 47 | m.register_uri('POST', "{}/{}".format(self.session.base_url, endpoint), status_code=codes.unauthorized) 48 | 49 | with self.assertRaises(InvalidCredentials) as _context: 50 | self.session.request('post', endpoint) 51 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2019 Elasticsearch BV 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /elastic_app_search/client.py: -------------------------------------------------------------------------------- 1 | import json 2 | import jwt 3 | from .request_session import RequestSession 4 | from .exceptions import InvalidDocument 5 | 6 | 7 | class Client: 8 | 9 | ELASTIC_APP_SEARCH_BASE_ENDPOINT = 'api.swiftype.com/api/as/v1' 10 | SIGNED_SEARCH_TOKEN_JWT_ALGORITHM = 'HS256' 11 | 12 | def __init__(self, host_identifier='', api_key='', 13 | base_endpoint=ELASTIC_APP_SEARCH_BASE_ENDPOINT, 14 | use_https=True, 15 | account_host_key='' # Deprecated - use host_identifier instead 16 | ): 17 | self.host_identifier = host_identifier or account_host_key 18 | self.account_host_key = self.host_identifier # Deprecated 19 | self.api_key = api_key 20 | 21 | uri_scheme = 'https' if use_https else 'http' 22 | host_prefix = host_identifier + '.' if host_identifier else '' 23 | base_url = "{}://{}{}".format(uri_scheme, host_prefix, base_endpoint) 24 | self.session = RequestSession(self.api_key, base_url) 25 | 26 | def get_documents(self, engine_name, document_ids): 27 | """ 28 | Retrieves documents by id from an engine. 29 | 30 | :param engine_name: Name of engine to get documents from. 31 | :param document_ids: Ids of documents to be returned. 32 | :return: Array of dictionaries representing documents. 33 | """ 34 | endpoint = "engines/{}/documents".format(engine_name) 35 | data = json.dumps(document_ids) 36 | return self.session.request('get', endpoint, data=data) 37 | 38 | def list_documents(self, engine_name, current=1, size=20): 39 | """ 40 | Lists all documents in engine. 41 | 42 | :param current: Page of documents 43 | :param size: Number of documents to return per page 44 | :return: List of documemts. 45 | """ 46 | data = { 'page': { 'current': current, 'size': size } } 47 | return self.session.request('get', "engines/{}/documents/list".format(engine_name), json=data) 48 | 49 | def index_document(self, engine_name, document): 50 | """ 51 | Create or update a document for an engine. Raises 52 | :class:`~elastic_app_search.exceptions.InvalidDocument` when the document 53 | has processing errors 54 | 55 | :param engine_name: Name of engine to index documents into. 56 | :param document: Hash representing a single document. 57 | :return: dict processed document status 58 | """ 59 | document_status = self.index_documents(engine_name, [document])[0] 60 | errors = document_status['errors'] 61 | if errors: 62 | raise InvalidDocument('; '.join(errors), document) 63 | 64 | return { 65 | key: document_status[key] 66 | for key in document_status 67 | if key != 'errors' 68 | } 69 | 70 | def index_documents(self, engine_name, documents): 71 | """ 72 | Create or update documents for an engine. 73 | 74 | :param engine_name: Name of engine to index documents into. 75 | :param documents: Hashes representing documents. 76 | :return: Array of document status dictionaries. Errors will be present 77 | in a document status with a key of `errors`. 78 | """ 79 | endpoint = "engines/{}/documents".format(engine_name) 80 | data = json.dumps(documents) 81 | 82 | return self.session.request('post', endpoint, data=data) 83 | 84 | def update_documents(self, engine_name, documents): 85 | """ 86 | Update a batch of documents for an engine. 87 | 88 | :param engine_name: Name of engine to index documents into. 89 | :param documents: Hashes representing documents. 90 | :return: Array of document status dictionaries. Errors will be present 91 | in a document status with a key of `errors`. 92 | """ 93 | endpoint = "engines/{}/documents".format(engine_name) 94 | data = json.dumps(documents) 95 | 96 | return self.session.request('patch', endpoint, data=data) 97 | 98 | def destroy_documents(self, engine_name, document_ids): 99 | """ 100 | Destroys documents by id for an engine. 101 | 102 | :param engine_name: Name of engine. 103 | :param document_ids: Array of document ids of documents to be destroyed. 104 | :return: 105 | """ 106 | endpoint = "engines/{}/documents".format(engine_name) 107 | data = json.dumps(document_ids) 108 | return self.session.request('delete', endpoint, data=data) 109 | 110 | def get_schema(self, engine_name): 111 | """ 112 | Get current schema for an engine. 113 | 114 | :param engine_name: Name of engine. 115 | :return: Schema. 116 | """ 117 | endpoint = "engines/{}/schema".format(engine_name) 118 | return self.session.request('get', endpoint) 119 | 120 | def update_schema(self, engine_name, schema): 121 | """ 122 | Create new schema fields or update the fields if they already exists. 123 | 124 | :param engine_name: Name of engine. 125 | :param schema: Schema to be updated as dict. 126 | :return: Updated schema. 127 | """ 128 | endpoint = "engines/{}/schema".format(engine_name) 129 | data = json.dumps(schema) 130 | return self.session.request('post', endpoint, data=data) 131 | 132 | def list_engines(self, current=1, size=20): 133 | """ 134 | Lists engines that the api key has access to. 135 | 136 | :param current: Page of engines 137 | :param size: Number of engines to return per page 138 | :return: List of dictionaries with key value pair corresponding to the 139 | name of the engine. 140 | """ 141 | data = { 'page': { 'current': current, 'size': size } } 142 | return self.session.request('get', 'engines', json=data) 143 | 144 | def get_engine(self, engine_name): 145 | """ 146 | Retrieves an engine by name. 147 | :param engine_name: Name of an existing engine. 148 | :return: A dictionary corresponding to the name of the engine. 149 | """ 150 | return self.session.request('get', "engines/{}".format(engine_name)) 151 | 152 | def create_engine(self, engine_name, language=None, options=None): 153 | """ 154 | Creates an engine with the specified name. 155 | :param engine_name: Name of the new engine. 156 | :param language: Language of the new engine. 157 | :param options: Engine configuration. 158 | :return: A dictionary corresponding to the new engine. 159 | """ 160 | data = { 'name': engine_name } 161 | if language is not None: 162 | data['language'] = language 163 | if options is not None: 164 | data.update(options) 165 | return self.session.request('post', 'engines', json=data) 166 | 167 | def destroy_engine(self, engine_name): 168 | """ 169 | Destroys an engine by name. 170 | :param engine_name: Name of an existing engine. 171 | :return: A dictionary with a single key of `deleted` and a value of 172 | True or False. 173 | """ 174 | return self.session.request('delete', "engines/{}".format(engine_name)) 175 | 176 | def list_synonym_sets(self, engine_name, current=1, size=20): 177 | """ 178 | Lists all synonym sets in engine. 179 | 180 | :param engine_name: Name of the engine. 181 | :param current: Page of synonym sets. 182 | :param size: Number of synonym sets to return per page. 183 | :return: List of synonym sets. 184 | """ 185 | data = { 'page': { 'current': current, 'size': size } } 186 | return self.session.request('get', "engines/{}/synonyms".format(engine_name), json=data) 187 | 188 | def get_synonym_set(self, engine_name, synonym_set_id): 189 | """ 190 | Get a single synonym set. 191 | 192 | :param engine_name: Name of the engine. 193 | :param synonym_set_id: ID of the synonym set. 194 | :return: A single synonym set. 195 | """ 196 | return self.session.request('get', "engines/{}/synonyms/{}".format(engine_name, synonym_set_id)) 197 | 198 | def create_synonym_set(self, engine_name, synonyms): 199 | """ 200 | Create a synonym set. 201 | 202 | :param engine_name: Name of the engine. 203 | :param synonyms: List of synonyms. 204 | :return: A list of synonyms that was created. 205 | """ 206 | data = { 'synonyms': synonyms } 207 | return self.session.request('post', "engines/{}/synonyms".format(engine_name), json=data) 208 | 209 | def update_synonym_set(self, engine_name, synonym_set_id, synonyms): 210 | """ 211 | Update an existing synonym set. 212 | 213 | :param engine_name: Name of the engine. 214 | :param synonym_set_id: ID of the synonym set to update. 215 | :param synonyms: The updated list of synonyms. 216 | :return: The synonym set that was updated. 217 | """ 218 | data = { 'synonyms': synonyms } 219 | return self.session.request('put', "engines/{}/synonyms/{}".format(engine_name, synonym_set_id), json=data) 220 | 221 | def destroy_synonym_set(self, engine_name, synonym_set_id): 222 | """ 223 | Destroy a synonym set. 224 | 225 | :param engine_name: Name of the engine. 226 | :param synonym_set_id: ID of the synonym set to be deleted. 227 | :return: Delete status. 228 | """ 229 | return self.session.request('delete', "engines/{}/synonyms/{}".format(engine_name, synonym_set_id)) 230 | 231 | def search(self, engine_name, query, options=None): 232 | """ 233 | Search an engine. See https://swiftype.com/documentation/app-search/ for more details 234 | on options and return values. 235 | 236 | :param engine_name: Name of engine to search over. 237 | :param query: Query string to search for. 238 | :param options: Dict of search options. 239 | """ 240 | endpoint = "engines/{}/search".format(engine_name) 241 | options = options or {} 242 | options['query'] = query 243 | return self.session.request('get', endpoint, json=options) 244 | 245 | def multi_search(self, engine_name, searches=None): 246 | """ 247 | Run multiple searches for documents on a single request. 248 | See https://swiftype.com/documentation/app-search/ for more details 249 | on options and return values. 250 | 251 | :param engine_name: Name of engine to search over. 252 | :param options: Array of search options. ex. {query: String, options: Dict} 253 | """ 254 | 255 | def build_options_from_search(search): 256 | if 'options' in search: 257 | options = search['options'] 258 | else: 259 | options = {} 260 | options['query'] = search['query'] 261 | return options 262 | 263 | endpoint = "engines/{}/multi_search".format(engine_name) 264 | options = { 265 | 'queries': list(map(build_options_from_search, searches)) 266 | } 267 | return self.session.request('get', endpoint, json=options) 268 | 269 | def query_suggestion(self, engine_name, query, options=None): 270 | """ 271 | Request Query Suggestions. See https://swiftype.com/documentation/app-search/ for more details 272 | on options and return values. 273 | 274 | :param engine_name: Name of engine to search over. 275 | :param query: Query string to search for. 276 | :param options: Dict of search options. 277 | """ 278 | endpoint = "engines/{}/query_suggestion".format(engine_name) 279 | options = options or {} 280 | options['query'] = query 281 | return self.session.request('get', endpoint, json=options) 282 | 283 | def click(self, engine_name, options): 284 | """ 285 | Sends a click event to the Elastic App Search Api, to track a click-through event. 286 | See https://swiftype.com/documentation/app-search/ for more details 287 | on options and return values. 288 | 289 | :param engine_name: Name of engine to search over. 290 | :param options: Dict of search options. 291 | """ 292 | endpoint = "engines/{}/click".format(engine_name) 293 | return self.session.request_ignore_response('post', endpoint, json=options) 294 | 295 | def create_meta_engine(self, engine_name, source_engines): 296 | data = { 297 | 'name': engine_name, 298 | 'source_engines': source_engines, 299 | 'type': 'meta' 300 | } 301 | return self.session.request('post', 'engines', json=data) 302 | 303 | def add_meta_engine_sources(self, engine_name, source_engines): 304 | endpoint = "engines/{}/source_engines".format(engine_name) 305 | return self.session.request('post', endpoint, json=source_engines) 306 | 307 | def delete_meta_engine_sources(self, engine_name, source_engines): 308 | endpoint = "engines/{}/source_engines".format(engine_name) 309 | return self.session.request('delete', endpoint, json=source_engines) 310 | 311 | def get_search_settings(self, engine_name): 312 | """ 313 | Get search settings for an engine. 314 | 315 | :param engine_name: Name of the engine. 316 | """ 317 | endpoint = "engines/{}/search_settings".format(engine_name) 318 | return self.session.request('get', endpoint) 319 | 320 | def update_search_settings(self, engine_name, search_settings): 321 | """ 322 | Update search settings for an engine. 323 | 324 | :param engine_name: Name of the engine. 325 | :param search_settings: New search settings JSON 326 | """ 327 | endpoint = "engines/{}/search_settings".format(engine_name) 328 | return self.session.request('put', endpoint, json=search_settings) 329 | 330 | def reset_search_settings(self, engine_name): 331 | """ 332 | Reset search settings to default for the given engine. 333 | 334 | :param engine_name: Name of the engine. 335 | """ 336 | endpoint = "engines/{}/search_settings/reset".format(engine_name) 337 | return self.session.request('post', endpoint) 338 | 339 | @staticmethod 340 | def create_signed_search_key(api_key, api_key_name, options): 341 | """ 342 | Creates a signed API key that will overwrite all search options (except 343 | filters) made with this key. 344 | 345 | :param api_key: An API key to use for this client. 346 | :param api_key_name: The unique name for the API Key 347 | :param options: Search options to override. 348 | :return: A JWT signed api token. 349 | """ 350 | options['api_key_name'] = api_key_name 351 | return jwt.encode(options, api_key, algorithm=Client.SIGNED_SEARCH_TOKEN_JWT_ALGORITHM) 352 | 353 | def get_api_logs(self, engine_name, options=None): 354 | """ 355 | Searches the API logs. 356 | 357 | :param engine_name: Name of engine. 358 | :param options: Dict of search options. 359 | """ 360 | endpoint = "engines/{}/logs/api".format(engine_name) 361 | options = options or {} 362 | return self.session.request('get', endpoint, json=options) 363 | 364 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **⚠️ This client is deprecated ⚠️** 2 | > 3 | > As of Enterprise Search version 7.10.0, we are directing users to the new [Enterprise Search Python Client](https://github.com/elastic/enterprise-search-python) and 4 | > deprecating this client. 5 | > 6 | > This client will be compatible with all Enterprise Search 7.x releases, but will not be compatible with 8.x releases. Our development effort on this project will 7 | > be limited to bug fixes. All future enhancements will be focused on the Enterprise Search Python Client. 8 | > 9 | > Thank you! - Elastic 10 | 11 | 12 |

15 |
16 | > A first-party Python client for building excellent, relevant search experiences with [Elastic App Search](https://www.elastic.co/products/app-search).
17 |
18 | ## Contents
19 |
20 | - [Getting started](#getting-started-)
21 | - [Dependencies](#dependencies)
22 | - [Versioning](#versioning)
23 | - [Usage](#usage)
24 | - [Running tests](#running-tests)
25 | - [FAQ](#faq-)
26 | - [Contribute](#contribute-)
27 | - [License](#license-)
28 |
29 | ---
30 |
31 | ## Getting started 🐣
32 |
33 | To install the client, use pip:
34 |
35 | ```python
36 | python -m pip install elastic-app-search
37 | ```
38 |
39 | You can also download the project source and run::
40 |
41 | ```python
42 | python setup.py install
43 | ```
44 |
45 | ## Dependencies
46 |
47 | - Python 2.7 / Python 3.3
48 | - [Requests](https://github.com/requests/requests)
49 | - [PyJWT](https://github.com/jpadilla/pyjwt)
50 |
51 | ## Versioning
52 |
53 | This client is versioned and released alongside App Search.
54 |
55 | To guarantee compatibility, use the most recent version of this library within the major version of the corresponding App Search implementation.
56 |
57 | For example, for App Search `7.3`, use `7.3` of this library or above, but not `8.0`.
58 |
59 | If you are using the [SaaS version available on swiftype.com](https://app.swiftype.com/as) of App Search, you should use the version 7.5.x of the client.
60 |
61 | ## Usage
62 |
63 | ### Instantiating a client
64 |
65 | Using this client assumes that you have already an instance of [Elastic App Search](https://www.elastic.co/products/app-search) up and running.
66 |
67 | The client can be instantiated using the `base_endpoint`, `api_key` and `use_https` parameters:
68 |
69 | ```python
70 | >>> from elastic_app_search import Client
71 | >>> client = Client(
72 | base_endpoint='localhost:3002/api/as/v1',
73 | api_key='private-mu75psc5egt9ppzuycnc2mc3',
74 | use_https=False
75 | )
76 | ```
77 |
78 | Notes:
79 |
80 | The `[api_key]` authenticates requests to the API.
81 | You can use any key type with the client, however each has a different scope.
82 | For more information on keys, check out the [documentation](https://swiftype.com/documentation/app-search/api/credentials).
83 |
84 | The `base_endpoint` must exclude the protocol and include the `api/as/v1` prefix. This can typically be found in the Credentials tab within the App Search Dashboard.
85 |
86 | Set `use_https` to `True` or `False` depending how your server is configured. Often times it will be `False` when running in development on `localhost` and `True` for production environments.
87 |
88 | The following is example of a configuration for Elastic Cloud:
89 |
90 | ```python
91 | >>> from elastic_app_search import Client
92 | >>> client = Client(
93 | base_endpoint='77bf13bc2e9948729af339a446b06ddcc.app-search.us-east-1.aws.found.io/api/as/v1',
94 | api_key='private-mu75psc5egt9ppzuycnc2mc3',
95 | use_https=True
96 | )
97 | ```
98 |
99 | #### Swiftype.com App Search users:
100 |
101 | When using the [SaaS version available on swiftype.com](https://app.swiftype.com/as) of App Search, you can configure the client using your `host_identifier` instead of the `base_endpoint` parameter.
102 | The `host_identifier` can be found within the [Credentials](https://app.swiftype.com/as#/credentials) menu.
103 |
104 | ```python
105 | >>> from elastic_app_search import Client
106 | >>> host_identifier = 'host-c5s2mj'
107 | >>> api_key = 'private-mu75psc5egt9ppzuycnc2mc3'
108 | >>> client = Client(host_identifier, api_key)
109 | ```
110 |
111 |
112 | ### Indexing: Creating or Updating a Single Document
113 |
114 | ```python
115 | >>> engine_name = 'favorite-videos'
116 | >>> document = {
117 | 'id': 'INscMGmhmX4',
118 | 'url': 'https://www.youtube.com/watch?v=INscMGmhmX4',
119 | 'title': 'The Original Grumpy Cat',
120 | 'body': 'A wonderful video of a magnificent cat.'
121 | }
122 | >>> client.index_document(engine_name, document)
123 | {'id': 'INscMGmhmX4'}
124 | ```
125 |
126 | ### Indexing: Creating or Updating Multiple Documents
127 |
128 | ```python
129 | >>> engine_name = 'favorite-videos'
130 | >>> documents = [
131 | {
132 | 'id': 'INscMGmhmX4',
133 | 'url': 'https://www.youtube.com/watch?v=INscMGmhmX4',
134 | 'title': 'The Original Grumpy Cat',
135 | 'body': 'A wonderful video of a magnificent cat.'
136 | },
137 | {
138 | 'id': 'JNDFojsd02',
139 | 'url': 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
140 | 'title': 'Another Grumpy Cat',
141 | 'body': 'A great video of another cool cat.'
142 | }
143 | ]
144 |
145 | >>> client.index_documents(engine_name, documents)
146 | [{'id': 'INscMGmhmX4', 'errors': []}, {'id': 'JNDFojsd02', 'errors': []}]
147 | ```
148 |
149 | ### Indexing: Updating documents (Partial Updates)
150 |
151 | ```python
152 | >>> engine_name = 'favorite-videos'
153 | >>> documents = [
154 | {
155 | 'id': 'INscMGmhmX4',
156 | 'title': 'Updated title'
157 | }
158 | ]
159 |
160 | >>> client.update_documents(engine_name, documents)
161 | ```
162 |
163 | ### Get Documents
164 |
165 | ```python
166 | >>> engine_name = 'favorite-videos'
167 | >>> client.get_documents(engine_name, ['INscMGmhmX4'])
168 | [{'id': 'INscMGmhmX4','url': 'https://www.youtube.com/watch?v=INscMGmhmX4','title': 'The Original Grumpy Cat','body': 'A wonderful video of a magnificent cat.'}]
169 | ```
170 |
171 | ### List Documents
172 |
173 | ```python
174 | >>> engine_name = 'favorite-videos'
175 | >>> client.list_documents(engine_name, current=1, size=20)
176 | {
177 | 'meta': {
178 | 'page': {
179 | 'current': 1,
180 | 'total_pages': 1,
181 | 'total_results': 2,
182 | 'size': 20
183 | }
184 | },
185 | 'results': [{'id': 'INscMGmhmX4','url': 'https://www.youtube.com/watch?v=INscMGmhmX4','title': 'The Original Grumpy Cat','body': 'A wonderful video of a magnificent cat.'}]
186 | }
187 | ```
188 |
189 | ### Destroy Documents
190 |
191 | ```python
192 | >>> engine_name = 'favorite-videos'
193 | >>> client.destroy_documents(engine_name, ['INscMGmhmX4'])
194 | [{'id': 'INscMGmhmX4','result': True}]
195 | ```
196 |
197 | ### Get Schema
198 |
199 | ```python
200 | >>> engine_name = 'favorite-videos'
201 | >>> client.get_schema(engine_name)
202 | {'name':'text', 'square_km': 'number', 'square_mi': 'text'}
203 | ```
204 |
205 | ### Create/Update Schema
206 |
207 | ```python
208 | >>> engine_name = 'favorite-videos'
209 | >>> client.update_schema(engine_name, {'square_km': 'text'})
210 | {'square_km': 'text'}
211 | >>> client.update_schema(engine_name, {'square_mi': 'text'})
212 | {'square_km': 'text', 'square_mi': 'text'}
213 | >>> client.update_schema(engine_name, {'square_km': 'number'})
214 | {'square_km': 'number', 'square_mi': 'text'}
215 | ```
216 |
217 | ### List Engines
218 |
219 | ```python
220 | >>> client.list_engines(current=1, size=20)
221 | {
222 | 'meta': {
223 | 'page': {
224 | 'current': 1,
225 | 'total_pages': 1,
226 | 'total_results': 2,
227 | 'size': 20
228 | }
229 | },
230 | 'results': [{'name': 'favorite-videos'}, {'name': 'another-engine'}]
231 | }
232 | ```
233 |
234 | ### Get an Engine
235 |
236 | ```python
237 | >>> client.get_engine('favorite-videos')
238 | {'name': 'favorite-videos'}
239 | ```
240 |
241 | ### Create an Engine
242 |
243 | ```python
244 | >>> client.create_engine('favorite-videos', 'en')
245 | {'name': 'favorite-videos', 'type': 'default', 'language': 'en'}
246 | ```
247 |
248 | ### Destroy an Engine
249 |
250 | ```python
251 | >>> client.destroy_engine('favorite-videos')
252 | {'deleted': True}
253 | ```
254 |
255 | ### List all synonym sets in an engine
256 |
257 | #### With default pagination (a page size of 20)
258 |
259 | ```python
260 | >>> client.list_synonym_sets('us-national-parks')
261 | {
262 | 'meta': {
263 | 'page': {
264 | 'current': 1,
265 | 'total_pages': 1,
266 | 'total_results': 3,
267 | 'size': 20
268 | }
269 | },
270 | 'results': [
271 | {
272 | 'id': 'syn-5b11ac66c9f9292013220ad3',
273 | 'synonyms': [
274 | 'park',
275 | 'trail'
276 | ]
277 | },
278 | {
279 | 'id': 'syn-5b11ac72c9f9296b35220ac9',
280 | 'synonyms': [
281 | 'protected',
282 | 'heritage'
283 | ]
284 | },
285 | {
286 | 'id': 'syn-5b11ac66c9f9292013220ad3',
287 | 'synonyms': [
288 | 'hectares',
289 | 'acres'
290 | ]
291 | }
292 | ]
293 | }
294 | ```
295 |
296 | #### With custom pagination
297 |
298 | ```python
299 | >>> client.list_synonym_sets('us-national-parks', size=1, current=1)
300 | {
301 | 'meta': {
302 | 'page': {
303 | 'current': 1,
304 | 'total_pages': 3,
305 | 'total_results': 3,
306 | 'size': 1
307 | }
308 | },
309 | 'results': [
310 | {
311 | 'id': 'syn-5b11ac66c9f9292013220ad3',
312 | 'synonyms': [
313 | 'park',
314 | 'trail'
315 | ]
316 | }
317 | ]
318 | }
319 | ```
320 |
321 | ### Get a single synonym set
322 |
323 | ```python
324 | >>> client.get_synonym_set('us-national-parks', 'syn-5b11ac66c9f9292013220ad3')
325 | {
326 | 'id': 'syn-5b11ac66c9f9292013220ad3',
327 | 'synonyms': [
328 | 'park',
329 | 'trail'
330 | ]
331 | }
332 | ```
333 |
334 | ### Create a synonym set
335 |
336 | ```python
337 | >>> client.create_synonym_set('us-national-parks', ['park', 'trail'])
338 | {
339 | 'id': 'syn-5b11ac72c9f9296b35220ac9',
340 | 'synonyms': [
341 | 'park',
342 | 'trail'
343 | ]
344 | }
345 | ```
346 |
347 | ### Update a synonym set
348 |
349 | ```python
350 | >>> client.update_synonym_set('us-national-parks', 'syn-5b11ac72c9f9296b35220ac9', ['park', 'trail', 'ground'])
351 | {
352 | 'id': 'syn-5b11ac72c9f9296b35220ac9',
353 | 'synonyms': [
354 | 'park',
355 | 'trail',
356 | 'ground'
357 | ]
358 | }
359 | ```
360 |
361 | ### Destroy a synonym set
362 |
363 | ```python
364 | >>> client.destroy_synonym_set('us-national-parks', 'syn-5b11ac66c9f9292013220ad3')
365 | {
366 | 'deleted': True
367 | }
368 | ```
369 |
370 | ### Searching
371 |
372 | ```python
373 | >>> client.search('favorite-videos', 'grumpy cat', {})
374 | {'meta': {'page': {'current': 1, 'total_pages': 1, 'total_results': 2, 'size': 10}, ...}, 'results': [...]}
375 | ```
376 |
377 | ### Multi-Search
378 |
379 | ```python
380 | >>> client.multi_search('favorite-videos', [{
381 | 'query': 'cat',
382 | 'options': { 'search_fields': { 'title': {} }}
383 | },{
384 | 'query': 'dog',
385 | 'options': { 'search_fields': { 'body': {} }}
386 | }])
387 | [{'meta': {...}, 'results': [...]}, {'meta': {...}, 'results': [...]}]
388 | ```
389 |
390 | ### Query Suggestion
391 |
392 | ```python
393 | >>> client.query_suggestion('favorite-videos', 'cat', {
394 | 'size': 10,
395 | 'types': {
396 | 'documents': {
397 | 'fields': ['title']
398 | }
399 | }
400 | })
401 | {'results': {'documents': [{'suggestion': 'cat'}]}, 'meta': {'request_id': '390be384ad5888353e1b32adcfaaf1c9'}}
402 | ```
403 |
404 | ### Clickthrough Tracking
405 |
406 | ```python
407 | >>> client.click(engine_name, {'query': 'cat', 'document_id': 'INscMGmhmX4'})
408 | ```
409 |
410 | ### Create a Signed Search Key
411 |
412 | Creating a search key that will only search over the body field.
413 |
414 | ```python
415 | >>> api_key = 'search-xxxxxxxxxxxxxxxxxxxxxxxx'
416 | >>> api_key_name = 'search-key' # This name must match the name of the key above from your App Search dashboard
417 | >>> signed_search_key = Client.create_signed_search_key(api_key, api_key_name, {'search_fields': { 'body': {}}})
418 | >>> client = Client(
419 | base_endpoint='localhost:3002/api/as/v1',
420 | api_key=signed_search_key,
421 | use_https=False
422 | )
423 | ```
424 |
425 | ### Create a Meta Engine
426 |
427 | ```python
428 | >>> client.create_meta_engine(
429 | engine_name=engine_name,
430 | source_engines=[
431 | 'source-engine-1',
432 | 'source-engine-2'
433 | ]
434 | )
435 | {'source_engines': ['source-engine-1', 'source-engine-2'], 'type': 'meta', 'name': 'my-meta-engine'}
436 | ```
437 |
438 | ### Add a Source Engine to a Meta Engine
439 |
440 | ```python
441 | >>> client.add_meta_engine_sources('my-meta-engine', ['source-engine-3'])
442 | {'source_engines': ['source-engine-1', 'source-engine-2', 'source-engine-3'], 'type': 'meta', 'name': 'my-meta-engine'}
443 | ```
444 |
445 | ### Remove a Source Engine from a Meta Engine
446 |
447 | ```python
448 | >>> client.delete_meta_engine_sources('my-meta-engine', ['source-engine-3'])
449 | {'source_engines': ['source-engine-1', 'source-engine-2'], 'type': 'meta', 'name': 'my-meta-engine'}
450 | ```
451 |
452 | ### Search the API logs
453 |
454 | ```python
455 | >>> client.get_api_logs('my-meta-engine', {
456 | "filters": {
457 | "date": {
458 | "from": "2020-03-30T00:00:00+00:00",
459 | "to": "2020-03-31T00:00:00+00:00"
460 | },
461 | "status": "429"
462 | }
463 | })
464 | {
465 | 'results': [],
466 | 'meta': {
467 | 'query': '',
468 | 'filters': {
469 | 'date': {
470 | 'from': '2020-03-27T00:00:00+00:00',
471 | 'to': '2020-03-31T00:00:00+00:00'
472 | },
473 | 'status': '429'
474 | },
475 | 'sort_direction': 'asc',
476 | 'page': {
477 | 'current': 1,
478 | 'total_pages': 0,
479 | 'total_results': 0,
480 | 'size': 10
481 | }
482 | }
483 | }
484 | ```
485 |
486 | ### Get search settings
487 |
488 | ```python
489 | >>> client.get_search_settings(engine_name='us-national-parks')
490 | {
491 | "search_fields": {
492 | "name": {
493 | "weight": 1
494 | },
495 | "description": {
496 | "weight": 1
497 | }
498 | },
499 | "result_fields": {
500 | "name": {
501 | "raw": {}
502 | },
503 | "description": {
504 | "raw": {}
505 | }
506 | },
507 | "boosts": {}
508 | }
509 | ```
510 |
511 | ### Update search settings
512 |
513 | ```python
514 | >>> client.update_search_settings(
515 | engine_name='us-national-parks',
516 | search_settings={
517 | "search_fields": {
518 | "name": {
519 | "weight": 2
520 | },
521 | "description": {
522 | "weight": 1
523 | }
524 | },
525 | "result_fields": {
526 | "name": {
527 | "raw": {}
528 | },
529 | "description": {
530 | "raw": {}
531 | }
532 | },
533 | "boosts": {}
534 | }
535 | )
536 | {
537 | "search_fields": {
538 | "name": {
539 | "weight": 2
540 | },
541 | "description": {
542 | "weight": 1
543 | }
544 | },
545 | "result_fields": {
546 | "name": {
547 | "raw": {}
548 | },
549 | "description": {
550 | "raw": {}
551 | }
552 | },
553 | "boosts": {}
554 | }
555 | ```
556 |
557 | ### Reset search settings
558 |
559 | ```python
560 | >>> client.reset_search_settings(engine_name='us-national-parks')
561 | {
562 | "search_fields": {
563 | "name": {
564 | "weight": 1
565 | },
566 | "description": {
567 | "weight": 1
568 | }
569 | },
570 | "boosts": {}
571 | }
572 | ```
573 |
574 | ## Running tests
575 |
576 | ```python
577 | python setup.py test
578 | ```
579 |
580 | ## FAQ 🔮
581 |
582 | ### Where do I report issues with the client?
583 |
584 | If something is not working as expected, please open an [issue](https://github.com/elastic/app-search-python/issues/new).
585 |
586 | ### Where can I learn more about App Search?
587 |
588 | Your best bet is to read the [documentation](https://swiftype.com/documentation/app-search).
589 |
590 | ### Where else can I go to get help?
591 |
592 | You can checkout the [Elastic App Search community discuss forums](https://discuss.elastic.co/c/app-search).
593 |
594 | ## Contribute 🚀
595 |
596 | We welcome contributors to the project. Before you begin, a couple notes:
597 |
598 | - Prior to opening a pull request, please create an issue to [discuss the scope of your proposal](https://github.com/elastic/app-search-python/issues).
599 | - Please write simple code and concise documentation, when appropriate.
600 |
601 | ## License 📗
602 |
603 | [Apache 2.0](https://github.com/elastic/app-search-python/blob/master/LICENSE.txt) © [Elastic](https://github.com/elastic)
604 |
605 | Thank you to all the [contributors](https://github.com/elastic/app-search-python/graphs/contributors)!
606 |
--------------------------------------------------------------------------------
/tests/test_client.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 | import requests_mock
3 | import json
4 |
5 | from elastic_app_search import Client
6 | from elastic_app_search.exceptions import InvalidDocument
7 |
8 |
9 | class TestClient(TestCase):
10 |
11 | def setUp(self):
12 | self.engine_name = 'some-engine-name'
13 | self.client = Client('host_identifier', 'api_key')
14 |
15 | self.document_index_url = "{}/{}".format(
16 | self.client.session.base_url,
17 | "engines/{}/documents".format(self.engine_name)
18 | )
19 |
20 | def test_deprecated_init_support_with_old_names(self):
21 | self.client = Client(
22 | account_host_key='host_identifier', api_key='api_key')
23 | self.assertEqual(self.client.account_host_key, 'host_identifier')
24 |
25 | def test_deprecated_init_support_with_new_names(self):
26 | self.client = Client(
27 | host_identifier='host_identifier', api_key='api_key')
28 | self.assertEqual(self.client.account_host_key, 'host_identifier')
29 |
30 | def test_deprecated_init_support_with_positional(self):
31 | self.client = Client('host_identifier', 'api_key',
32 | 'example.com', False)
33 | self.assertEqual(self.client.account_host_key, 'host_identifier')
34 |
35 | def test_host_identifier_is_optional(self):
36 | client = Client('', 'api_key', 'localhost:3002/api/as/v1', False)
37 | query = 'query'
38 |
39 | with requests_mock.Mocker() as m:
40 | url = "http://localhost:3002/api/as/v1/engines/some-engine-name/search"
41 | m.register_uri('GET', url, json={}, status_code=200)
42 | client.search(self.engine_name, query, {})
43 |
44 | def test_index_document_processing_error(self):
45 | invalid_document = {'id': 'something', 'bad': {'no': 'nested'}}
46 | error = 'some processing error'
47 | stubbed_return = [{'id': 'something', 'errors': [error]}]
48 | with requests_mock.Mocker() as m:
49 | m.register_uri('POST', self.document_index_url,
50 | json=stubbed_return, status_code=200)
51 |
52 | with self.assertRaises(InvalidDocument) as context:
53 | self.client.index_document(self.engine_name, invalid_document)
54 | self.assertEqual(str(context.exception), error)
55 |
56 | def test_index_document_no_error_key_in_response(self):
57 | document_without_id = {'body': 'some value'}
58 | stubbed_return = [{'id': 'auto generated', 'errors': []}]
59 |
60 | with requests_mock.Mocker() as m:
61 | m.register_uri('POST', self.document_index_url,
62 | json=stubbed_return, status_code=200)
63 | response = self.client.index_document(
64 | self.engine_name, document_without_id)
65 | self.assertEqual(response, {'id': 'auto generated'})
66 |
67 | def test_index_documents(self):
68 | id = 'INscMGmhmX4'
69 | valid_document = {'id': id}
70 | other_document = {'body': 'some value'}
71 |
72 | expected_return = [
73 | {'id': id, 'errors': []},
74 | {'id': 'some autogenerated id', 'errors': []}
75 | ]
76 |
77 | with requests_mock.Mocker() as m:
78 | m.register_uri('POST', self.document_index_url,
79 | json=expected_return, status_code=200)
80 | response = self.client.index_documents(
81 | self.engine_name, [valid_document, other_document])
82 | self.assertEqual(response, expected_return)
83 |
84 | def test_update_documents(self):
85 | id = 'INscMGmhmX4'
86 | valid_document = {'id': id}
87 | other_document = {'body': 'some value'}
88 |
89 | expected_return = [
90 | {'id': id, 'errors': []},
91 | {'id': 'some autogenerated id', 'errors': []}
92 | ]
93 |
94 | with requests_mock.Mocker() as m:
95 | m.register_uri('PATCH', self.document_index_url,
96 | json=expected_return, status_code=200)
97 | response = self.client.update_documents(
98 | self.engine_name, [valid_document, other_document])
99 | self.assertEqual(response, expected_return)
100 |
101 | def test_get_documents(self):
102 | id = 'INscMGmhmX4'
103 | expected_return = [
104 | {
105 | 'id': id,
106 | 'url': 'http://www.youtube.com/watch?v=v1uyQZNg2vE',
107 | 'title': 'The Original Grumpy Cat',
108 | 'body': 'this is a test'
109 | }
110 | ]
111 |
112 | with requests_mock.Mocker() as m:
113 | m.register_uri('GET', self.document_index_url,
114 | json=expected_return, status_code=200)
115 | response = self.client.get_documents(self.engine_name, [id])
116 | self.assertEqual(response, expected_return)
117 |
118 | def test_list_documents(self):
119 | expected_return = {
120 | 'meta': {
121 | 'page': {'current': 1, 'total_results': 1, 'total_pages': 1, 'size': 20},
122 | 'results': [
123 | {'body': 'this is a test', 'id': '1'},
124 | {'body': 'this is also a test', 'id': '2'}
125 | ]
126 | }
127 | }
128 |
129 | def match_request_text(request):
130 | data = json.loads(request.text)
131 | return data["page"]["current"] == 1 and data["page"]["size"] == 20
132 |
133 | with requests_mock.Mocker() as m:
134 | url = "{}/engines/{}/documents/list".format(
135 | self.client.session.base_url, self.engine_name)
136 | m.register_uri('GET',
137 | url,
138 | additional_matcher=match_request_text,
139 | json=expected_return,
140 | status_code=200
141 | )
142 |
143 | response = self.client.list_documents(self.engine_name)
144 | self.assertEqual(response, expected_return)
145 |
146 | def test_destroy_documents(self):
147 | id = 'INscMGmhmX4'
148 | expected_return = [
149 | {'id': id, 'result': True}
150 | ]
151 |
152 | with requests_mock.Mocker() as m:
153 | m.register_uri('DELETE', self.document_index_url,
154 | json=expected_return, status_code=200)
155 | response = self.client.destroy_documents(self.engine_name, [id])
156 | self.assertEqual(response, expected_return)
157 |
158 | def test_get_schema(self):
159 | expected_return = {
160 | 'square_km': 'text'
161 | }
162 |
163 | with requests_mock.Mocker() as m:
164 | url = "{}/engines/{}/schema".format(
165 | self.client.session.base_url, self.engine_name)
166 | m.register_uri('GET',
167 | url,
168 | json=expected_return,
169 | status_code=200
170 | )
171 |
172 | response = self.client.get_schema(self.engine_name)
173 | self.assertEqual(response, expected_return)
174 |
175 | def test_update_schema(self):
176 | expected_return = {
177 | 'square_mi': 'number',
178 | 'square_km': 'number'
179 | }
180 |
181 | with requests_mock.Mocker() as m:
182 | url = "{}/engines/{}/schema".format(
183 | self.client.session.base_url, self.engine_name)
184 | m.register_uri('POST',
185 | url,
186 | json=expected_return,
187 | status_code=200
188 | )
189 |
190 | response = self.client.update_schema(
191 | self.engine_name, expected_return)
192 | self.assertEqual(response, expected_return)
193 |
194 | def test_list_engines(self):
195 | expected_return = [
196 | {'name': 'myawesomeengine'}
197 | ]
198 |
199 | def match_request_text(request):
200 | data = json.loads(request.text)
201 | return data["page"]["current"] == 1 and data["page"]["size"] == 20
202 |
203 | with requests_mock.Mocker() as m:
204 | url = "{}/{}".format(self.client.session.base_url, 'engines')
205 | m.register_uri('GET',
206 | url,
207 | additional_matcher=match_request_text,
208 | json=expected_return,
209 | status_code=200
210 | )
211 | response = self.client.list_engines()
212 | self.assertEqual(response, expected_return)
213 |
214 | def test_list_engines_with_paging(self):
215 | expected_return = [
216 | {'name': 'myawesomeengine'}
217 | ]
218 |
219 | def match_request_text(request):
220 | data = json.loads(request.text)
221 | return data["page"]["current"] == 10 and data["page"]["size"] == 2
222 |
223 | with requests_mock.Mocker() as m:
224 | url = "{}/{}".format(self.client.session.base_url, 'engines')
225 | m.register_uri(
226 | 'GET',
227 | url,
228 | additional_matcher=match_request_text,
229 | json=expected_return,
230 | status_code=200
231 | )
232 | response = self.client.list_engines(current=10, size=2)
233 | self.assertEqual(response, expected_return)
234 |
235 | def test_get_engine(self):
236 | engine_name = 'myawesomeengine'
237 | expected_return = [
238 | {'name': engine_name}
239 | ]
240 |
241 | with requests_mock.Mocker() as m:
242 | url = "{}/{}/{}".format(self.client.session.base_url,
243 | 'engines',
244 | engine_name)
245 | m.register_uri('GET', url, json=expected_return, status_code=200)
246 | response = self.client.get_engine(engine_name)
247 | self.assertEqual(response, expected_return)
248 |
249 | def test_create_engine(self):
250 | engine_name = 'myawesomeengine'
251 | expected_return = {'name': engine_name, 'language': 'en'}
252 |
253 | with requests_mock.Mocker() as m:
254 | url = "{}/{}".format(self.client.session.base_url, 'engines')
255 | m.register_uri('POST', url, json=expected_return, status_code=200)
256 | response = self.client.create_engine(
257 | engine_name=engine_name, language='en')
258 | self.assertEqual(response, expected_return)
259 |
260 | def test_create_engine_with_options(self):
261 | engine_name = 'myawesomeengine'
262 | expected_return = {'name': engine_name, 'type': 'meta',
263 | 'source_engines': [
264 | 'source-engine-1',
265 | 'source-engine-2'
266 | ]}
267 |
268 | with requests_mock.Mocker() as m:
269 | url = "{}/{}".format(self.client.session.base_url, 'engines')
270 | m.register_uri('POST', url, json=expected_return, status_code=200)
271 | response = self.client.create_engine(
272 | engine_name=engine_name, options={
273 | 'type': 'meta',
274 | 'source_engines': [
275 | 'source-engine-1',
276 | 'source-engine-2'
277 | ]
278 | })
279 | self.assertEqual(response, expected_return)
280 |
281 | def test_destroy_engine(self):
282 | engine_name = 'myawesomeengine'
283 | expected_return = {'deleted': True}
284 |
285 | with requests_mock.Mocker() as m:
286 | url = "{}/{}/{}".format(self.client.session.base_url,
287 | 'engines',
288 | engine_name)
289 | m.register_uri('DELETE', url, json=expected_return,
290 | status_code=200)
291 | response = self.client.destroy_engine(engine_name)
292 | self.assertEqual(response, expected_return)
293 |
294 | def test_list_synonym_sets(self):
295 | expected_return = {
296 | 'meta': {
297 | 'page': {
298 | 'current': 1,
299 | 'total_pages': 1,
300 | 'total_results': 3,
301 | 'size': 20
302 | }
303 | },
304 | 'results': [
305 | {
306 | 'id': 'syn-5b11ac66c9f9292013220ad3',
307 | 'synonyms': [
308 | 'park',
309 | 'trail'
310 | ]
311 | },
312 | {
313 | 'id': 'syn-5b11ac72c9f9296b35220ac9',
314 | 'synonyms': [
315 | 'protected',
316 | 'heritage'
317 | ]
318 | },
319 | {
320 | 'id': 'syn-5b11ac66c9f9292013220ad3',
321 | 'synonyms': [
322 | 'hectares',
323 | 'acres'
324 | ]
325 | }
326 | ]
327 | }
328 |
329 | with requests_mock.Mocker() as m:
330 | url = "{}/engines/{}/synonyms".format(
331 | self.client.session.base_url,
332 | self.engine_name
333 | )
334 |
335 | def match_request_text(request):
336 | data = json.loads(request.text)
337 | return data["page"]["current"] == 1 and data["page"]["size"] == 20
338 |
339 | m.register_uri(
340 | 'GET',
341 | url,
342 | additional_matcher=match_request_text,
343 | json=expected_return,
344 | status_code=200
345 | )
346 |
347 | response = self.client.list_synonym_sets(self.engine_name)
348 |
349 | def test_get_synonym_set(self):
350 | synonym_id = 'syn-5b11ac66c9f9292013220ad3'
351 | expected_return = {
352 | 'id': synonym_id,
353 | 'synonyms': [
354 | 'park',
355 | 'trail'
356 | ]
357 | }
358 |
359 | with requests_mock.Mocker() as m:
360 | url = "{}/engines/{}/synonyms/{}".format(
361 | self.client.session.base_url,
362 | self.engine_name,
363 | synonym_id
364 | )
365 | m.register_uri(
366 | 'GET',
367 | url,
368 | json=expected_return,
369 | status_code=200
370 | )
371 |
372 | response = self.client.get_synonym_set(
373 | self.engine_name,
374 | synonym_id
375 | )
376 | self.assertEqual(response, expected_return)
377 |
378 | def test_create_synonym_set(self):
379 | synonym_set = ['park', 'trail']
380 | expected_return = {
381 | 'id': 'syn-5b11ac72c9f9296b35220ac9',
382 | 'synonyms': [
383 | 'park',
384 | 'trail'
385 | ]
386 | }
387 |
388 | with requests_mock.Mocker() as m:
389 | url = "{}/engines/{}/synonyms".format(
390 | self.client.session.base_url,
391 | self.engine_name
392 | )
393 | m.register_uri(
394 | 'POST',
395 | url,
396 | json=expected_return,
397 | status_code=200
398 | )
399 |
400 | response = self.client.create_synonym_set(
401 | self.engine_name,
402 | synonym_set
403 | )
404 | self.assertEqual(response, expected_return)
405 |
406 | def test_update_synonym_set(self):
407 | synonym_id = 'syn-5b11ac72c9f9296b35220ac9'
408 | synonym_set = ['park', 'trail', 'ground']
409 | expected_return = {
410 | 'id': synonym_id,
411 | 'synonyms': [
412 | 'park',
413 | 'trail',
414 | 'ground'
415 | ]
416 | }
417 |
418 | with requests_mock.Mocker() as m:
419 | url = "{}/engines/{}/synonyms/{}".format(
420 | self.client.session.base_url,
421 | self.engine_name,
422 | synonym_id
423 | )
424 | m.register_uri(
425 | 'PUT',
426 | url,
427 | json=expected_return,
428 | status_code=200
429 | )
430 |
431 | response = self.client.update_synonym_set(
432 | self.engine_name,
433 | synonym_id,
434 | synonym_set
435 | )
436 | self.assertEqual(response, expected_return)
437 |
438 | def test_destroy_synonym_set(self):
439 | synonym_id = 'syn-5b11ac66c9f9292013220ad3'
440 | expected_return = {
441 | 'deleted': True
442 | }
443 |
444 | with requests_mock.Mocker() as m:
445 | url = "{}/engines/{}/synonyms/{}".format(
446 | self.client.session.base_url,
447 | self.engine_name,
448 | synonym_id
449 | )
450 | m.register_uri(
451 | 'DELETE',
452 | url,
453 | json=expected_return,
454 | status_code=200
455 | )
456 |
457 | response = self.client.destroy_synonym_set(
458 | self.engine_name,
459 | synonym_id
460 | )
461 | self.assertEqual(response, expected_return)
462 |
463 | def test_search(self):
464 | query = 'query'
465 | expected_return = {'meta': {}, 'results': []}
466 |
467 | with requests_mock.Mocker() as m:
468 | url = "{}/{}".format(
469 | self.client.session.base_url,
470 | "engines/{}/search".format(self.engine_name)
471 | )
472 | m.register_uri('GET', url, json=expected_return, status_code=200)
473 | response = self.client.search(self.engine_name, query, {})
474 | self.assertEqual(response, expected_return)
475 |
476 | def test_multi_search(self):
477 | expected_return = [{'meta': {}, 'results': []},
478 | {'meta': {}, 'results': []}]
479 |
480 | with requests_mock.Mocker() as m:
481 | url = "{}/{}".format(
482 | self.client.session.base_url,
483 | "engines/{}/multi_search".format(self.engine_name)
484 | )
485 | m.register_uri('GET', url, json=expected_return, status_code=200)
486 | response = self.client.multi_search(self.engine_name, {})
487 | self.assertEqual(response, expected_return)
488 |
489 | def test_query_suggestion(self):
490 | query = 'query'
491 | expected_return = {'meta': {}, 'results': {}}
492 |
493 | with requests_mock.Mocker() as m:
494 | url = "{}/{}".format(
495 | self.client.session.base_url,
496 | "engines/{}/query_suggestion".format(self.engine_name)
497 | )
498 | m.register_uri('GET', url, json=expected_return, status_code=200)
499 | response = self.client.query_suggestion(
500 | self.engine_name, query, {})
501 | self.assertEqual(response, expected_return)
502 |
503 | def test_click(self):
504 | with requests_mock.Mocker() as m:
505 | url = "{}/{}".format(
506 | self.client.session.base_url,
507 | "engines/{}/click".format(self.engine_name)
508 | )
509 | m.register_uri('POST', url, json={}, status_code=200)
510 | self.client.click(self.engine_name, {
511 | 'query': 'cat', 'document_id': 'INscMGmhmX4'})
512 |
513 | def test_create_meta_engine(self):
514 | source_engines = ['source-engine-1', 'source-engine-2']
515 | expected_return = {'source_engines': source_engines,
516 | 'type': 'meta', 'name': self.engine_name}
517 |
518 | with requests_mock.Mocker() as m:
519 | url = "{}/{}".format(self.client.session.base_url, 'engines')
520 | m.register_uri('POST', url, json=expected_return, status_code=200)
521 | response = self.client.create_meta_engine(
522 | self.engine_name, source_engines)
523 | self.assertEqual(response, expected_return)
524 |
525 | def test_add_meta_engine_sources(self):
526 | target_source_engine_name = 'source-engine-3'
527 | expected_return = {'source_engines': [
528 | 'source-engine-1', 'source-engine-2', target_source_engine_name], 'type': 'meta', 'name': self.engine_name}
529 |
530 | with requests_mock.Mocker() as m:
531 | url = "{}/{}".format(
532 | self.client.session.base_url,
533 | "engines/{}/source_engines".format(self.engine_name)
534 | )
535 | m.register_uri('POST', url, json=expected_return, status_code=200)
536 | response = self.client.add_meta_engine_sources(
537 | self.engine_name, [target_source_engine_name])
538 | self.assertEqual(response, expected_return)
539 |
540 | def test_delete_meta_engine_sources(self):
541 | source_engine_name = 'source-engine-3'
542 | expected_return = {'source_engines': [
543 | 'source-engine-1', 'source-engine-2'], 'type': 'meta', 'name': self.engine_name}
544 |
545 | with requests_mock.Mocker() as m:
546 | url = "{}/{}".format(
547 | self.client.session.base_url,
548 | "engines/{}/source_engines".format(self.engine_name)
549 | )
550 | m.register_uri('DELETE', url, json=expected_return,
551 | status_code=200)
552 | response = self.client.delete_meta_engine_sources(
553 | self.engine_name, [source_engine_name])
554 | self.assertEqual(response, expected_return)
555 |
556 | def test_get_api_logs(self):
557 | expected_return = {'meta': {}, 'results': []}
558 |
559 | with requests_mock.Mocker() as m:
560 | url = "{}/{}".format(
561 | self.client.session.base_url,
562 | "engines/{}/logs/api".format(self.engine_name)
563 | )
564 | m.register_uri('GET', url, json=expected_return, status_code=200)
565 | response = self.client.get_api_logs(self.engine_name, options={})
566 | self.assertEqual(response, expected_return)
567 |
568 | def test_get_search_settings(self):
569 | expected_return = {
570 | "search_fields": {
571 | "name": {
572 | "weight": 1
573 | },
574 | "description": {
575 | "weight": 1
576 | }
577 | },
578 | "result_fields": {
579 | "name": {
580 | "raw": {}
581 | },
582 | "description": {
583 | "raw": {}
584 | }
585 | },
586 | "boosts": {}
587 | }
588 |
589 | with requests_mock.Mocker() as m:
590 | url = "{}/engines/{}/search_settings".format(
591 | self.client.session.base_url,
592 | self.engine_name
593 | )
594 | m.register_uri('GET', url, json=expected_return, status_code=200)
595 | response = self.client.get_search_settings(self.engine_name)
596 | self.assertEqual(response, expected_return)
597 |
598 | def test_update_search_settings(self):
599 | expected_return = {
600 | "search_fields": {
601 | "name": {
602 | "weight": 2
603 | },
604 | "description": {
605 | "weight": 1
606 | }
607 | },
608 | "result_fields": {
609 | "name": {
610 | "raw": {}
611 | },
612 | "description": {
613 | "raw": {}
614 | }
615 | },
616 | "boosts": {}
617 | }
618 |
619 | with requests_mock.Mocker() as m:
620 | url = "{}/engines/{}/search_settings".format(
621 | self.client.session.base_url,
622 | self.engine_name
623 | )
624 | m.register_uri('PUT', url, json=expected_return, status_code=200)
625 | response = self.client.update_search_settings(
626 | engine_name=self.engine_name,
627 | search_settings=expected_return
628 | )
629 | self.assertEqual(response, expected_return)
630 |
631 | def test_reset_search_settings(self):
632 | expected_return = {
633 | "search_fields": {
634 | "name": {
635 | "weight": 1
636 | },
637 | "description": {
638 | "weight": 1
639 | }
640 | },
641 | "result_fields": {
642 | "name": {
643 | "raw": {}
644 | },
645 | "description": {
646 | "raw": {}
647 | }
648 | },
649 | "boosts": {}
650 | }
651 |
652 | with requests_mock.Mocker() as m:
653 | url = "{}/engines/{}/search_settings/reset".format(
654 | self.client.session.base_url,
655 | self.engine_name
656 | )
657 | m.register_uri('POST', url, json=expected_return, status_code=200)
658 | response = self.client.reset_search_settings(
659 | engine_name=self.engine_name
660 | )
661 | self.assertEqual(response, expected_return)
662 |
--------------------------------------------------------------------------------