From d412583b35ff85281697190d2424a56603cbc08d Mon Sep 17 00:00:00 2001 From: Paul-Louis NECH Date: Fri, 4 Jun 2021 17:22:21 +0200 Subject: [PATCH 1/5] feat(AnswersClient): V1 implementation --- algoliasearch/answers_client.py | 63 +++++++++++++++++++++++++++ algoliasearch/answers_client_async.py | 43 ++++++++++++++++++ algoliasearch/configs.py | 6 +++ tests/features/test_answers_client.py | 42 ++++++++++++++++++ tests/helpers/factory.py | 10 +++++ 5 files changed, 164 insertions(+) create mode 100644 algoliasearch/answers_client.py create mode 100644 algoliasearch/answers_client_async.py create mode 100644 tests/features/test_answers_client.py diff --git a/algoliasearch/answers_client.py b/algoliasearch/answers_client.py new file mode 100644 index 000000000..e3191f692 --- /dev/null +++ b/algoliasearch/answers_client.py @@ -0,0 +1,63 @@ +from typing import Optional, Union, Dict, Any + +from algoliasearch.configs import AnswersConfig +from algoliasearch.helpers import is_async_available +from algoliasearch.http.request_options import RequestOptions +from algoliasearch.http.requester import Requester +from algoliasearch.http.transporter import Transporter +from algoliasearch.http.verb import Verb + + +class AnswersClient(object): + def __init__(self, transporter, config): + # type: (Transporter, AnswersConfig) -> None + + self._transporter = transporter + self._config = config + + @staticmethod + def create(app_id=None, api_key=None): + # type: (Optional[str], Optional[str]) -> AnswersClient # noqa: E501 + + config = AnswersConfig(app_id, api_key) + + return AnswersClient.create_with_config(config) + + @staticmethod + def create_with_config(config): + # type: (AnswersConfig) -> AnswersClient + + requester = Requester() + transporter = Transporter(requester, config) + + client = AnswersClient(transporter, config) + + if is_async_available(): + from algoliasearch.answers_client_async import ( + AnswersClientAsync, + ) + from algoliasearch.http.transporter_async import TransporterAsync + from algoliasearch.http.requester_async import RequesterAsync + + return AnswersClientAsync( + client, TransporterAsync(RequesterAsync(), config), config + ) + + return client + + def predict( + self, index_name, answers_parameters, request_options=None + ): # noqa: E501 + # type: (str, dict, Optional[Union[dict, RequestOptions]]) -> dict + + return self._transporter.write( + Verb.POST, + "1/answers/{}/prediction".format(index_name), + answers_parameters, + request_options, + ) + + def close(self): + # type: () -> None + + return self._transporter.close() # type: ignore diff --git a/algoliasearch/answers_client_async.py b/algoliasearch/answers_client_async.py new file mode 100644 index 000000000..c14d57358 --- /dev/null +++ b/algoliasearch/answers_client_async.py @@ -0,0 +1,43 @@ +import types +import asyncio +from typing import Optional, Type + +from algoliasearch.answers_client import AnswersClient +from algoliasearch.configs import AnswersConfig +from algoliasearch.helpers_async import _create_async_methods_in +from algoliasearch.http.transporter_async import TransporterAsync + + +class AnswersClientAsync(AnswersClient): + def __init__(self, answers_client, transporter, search_config): + # type: (AnswersClient, TransporterAsync, AnswersConfig) -> None # noqa: E501 + + self._transporter_async = transporter + + super(AnswersClientAsync, self).__init__( + answers_client._transporter, search_config + ) + + client = AnswersClient(transporter, search_config) + + _create_async_methods_in(self, client) + + @asyncio.coroutine + def __aenter__(self): + # type: () -> AnswersClientAsync # type: ignore + + return self # type: ignore + + @asyncio.coroutine + def __aexit__(self, exc_type, exc, tb): # type: ignore + # type: (Optional[Type[BaseException]], Optional[BaseException],Optional[types.TracebackType]) -> None # noqa: E501 + + yield from self.close_async() # type: ignore + + @asyncio.coroutine + def close_async(self): # type: ignore + # type: () -> None + + super().close() + + yield from self._transporter_async.close() # type: ignore diff --git a/algoliasearch/configs.py b/algoliasearch/configs.py index 02cc9a08d..40b87a427 100644 --- a/algoliasearch/configs.py +++ b/algoliasearch/configs.py @@ -124,3 +124,9 @@ def build_hosts(self): return HostsCollection( [Host("{}.{}.{}".format("recommendation", self._region, "algolia.com"))] ) + +class AnswersConfig(SearchConfig): + def __init__(self, app_id=None, api_key=None): + # type: (Optional[str], Optional[str]) -> None + + super(AnswersConfig, self).__init__(app_id, api_key) diff --git a/tests/features/test_answers_client.py b/tests/features/test_answers_client.py new file mode 100644 index 000000000..2f7915964 --- /dev/null +++ b/tests/features/test_answers_client.py @@ -0,0 +1,42 @@ +import unittest + +from algoliasearch.exceptions import RequestException +from tests.helpers.factory import Factory as F + + +class TestAnswersClient(unittest.TestCase): + def setUp(self): + self.search_client = F.search_client() + self.client = F.answers_client() + self.index = F.index(self.search_client, self._testMethodName) + self.index.save_objects([ + { + "name": "Something", + "description": "Your usage of demo datasets is usually more creative :')", + "objectID": 0, + }, { + "name": "Another thing", + "description": "This is creative, but unused. ;)", + "objectID": 1, + }, + ]).wait() + + def tearDown(self): + self.client.close() + + def test_answers(self): + data = { + "query": "Any usage?", + "queryLanguages": ["en"], + "attributesForPrediction": ["title", "description"], + "nbHits": 2 + } + + try: + response = self.client.predict(self.index.name, data) + print(response) + self.assertTrue("hits" in response) + self.assertIn("usage", response["hits"][0]["_answer"]["extract"]) + self.assertEqual("0", response["hits"][0]["objectID"]) + except RequestException as err: + self.fail(err) # noqa: E501 diff --git a/tests/helpers/factory.py b/tests/helpers/factory.py index 581f45837..ec868bd8d 100644 --- a/tests/helpers/factory.py +++ b/tests/helpers/factory.py @@ -8,6 +8,7 @@ from typing import Optional from algoliasearch.analytics_client import AnalyticsClient +from algoliasearch.answers_client import AnswersClient from algoliasearch.insights_client import InsightsClient from algoliasearch.search_client import SearchClient, SearchConfig from algoliasearch.recommendation_client import RecommendationClient @@ -87,6 +88,15 @@ def recommendation_client(app_id=None, api_key=None): return Factory.decide(RecommendationClient.create(app_id, api_key)) + @staticmethod + def answers_client(app_id=None, api_key=None): + # type: (Optional[str], Optional[str]) -> AnswersClient + + app_id = app_id if app_id is not None else Factory.get_app_id() + api_key = api_key if api_key is not None else Factory.get_api_key() + + return Factory.decide(AnswersClient.create(app_id, api_key)) + @staticmethod def insights_client(app_id=None, api_key=None): # type: (Optional[str], Optional[str]) -> InsightsClient From 6336e0e6074835d242241b29245fde83c949c1ad Mon Sep 17 00:00:00 2001 From: Paul-Louis NECH Date: Fri, 4 Jun 2021 17:24:18 +0200 Subject: [PATCH 2/5] chore: update Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9f1007e2..d81f57074 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). # Release Notes ## [Unreleased](https://github.com/algolia/algoliasearch-client-python/compare/v2.5.0...master) +- Add support for Answers API [#528](https://github.com/algolia/algoliasearch-client-python/pull/528) ## [v2.5.0](https://github.com/algolia/algoliasearch-client-python/compare/v2.4.0...v2.5.0) From 93e71385e71a2ec43123cb2d8f298bcbd6bb64a8 Mon Sep 17 00:00:00 2001 From: Paul-Louis NECH Date: Fri, 4 Jun 2021 17:26:47 +0200 Subject: [PATCH 3/5] chore: formatting --- algoliasearch/answers_client.py | 8 ++------ algoliasearch/answers_client_async.py | 4 +--- algoliasearch/configs.py | 13 ++++--------- tests/features/test_answers_client.py | 27 +++++++++++++++------------ 4 files changed, 22 insertions(+), 30 deletions(-) diff --git a/algoliasearch/answers_client.py b/algoliasearch/answers_client.py index e3191f692..8f3844c7d 100644 --- a/algoliasearch/answers_client.py +++ b/algoliasearch/answers_client.py @@ -39,15 +39,11 @@ def create_with_config(config): from algoliasearch.http.transporter_async import TransporterAsync from algoliasearch.http.requester_async import RequesterAsync - return AnswersClientAsync( - client, TransporterAsync(RequesterAsync(), config), config - ) + return AnswersClientAsync(client, TransporterAsync(RequesterAsync(), config), config) return client - def predict( - self, index_name, answers_parameters, request_options=None - ): # noqa: E501 + def predict(self, index_name, answers_parameters, request_options=None): # noqa: E501 # type: (str, dict, Optional[Union[dict, RequestOptions]]) -> dict return self._transporter.write( diff --git a/algoliasearch/answers_client_async.py b/algoliasearch/answers_client_async.py index c14d57358..89660104c 100644 --- a/algoliasearch/answers_client_async.py +++ b/algoliasearch/answers_client_async.py @@ -14,9 +14,7 @@ def __init__(self, answers_client, transporter, search_config): self._transporter_async = transporter - super(AnswersClientAsync, self).__init__( - answers_client._transporter, search_config - ) + super(AnswersClientAsync, self).__init__(answers_client._transporter, search_config) client = AnswersClient(transporter, search_config) diff --git a/algoliasearch/configs.py b/algoliasearch/configs.py index 40b87a427..ed5777748 100644 --- a/algoliasearch/configs.py +++ b/algoliasearch/configs.py @@ -89,9 +89,7 @@ def __init__(self, app_id=None, api_key=None, region=None): def build_hosts(self): # type: () -> HostsCollection - return HostsCollection( - [Host("{}.{}.{}".format("analytics", self._region, "algolia.com"))] - ) + return HostsCollection([Host("{}.{}.{}".format("analytics", self._region, "algolia.com"))]) class InsightsConfig(Config): @@ -105,9 +103,7 @@ def __init__(self, app_id=None, api_key=None, region=None): def build_hosts(self): # type: () -> HostsCollection - return HostsCollection( - [Host("{}.{}.{}".format("insights", self._region, "algolia.io"))] - ) + return HostsCollection([Host("{}.{}.{}".format("insights", self._region, "algolia.io"))]) class RecommendationConfig(Config): @@ -121,9 +117,8 @@ def __init__(self, app_id=None, api_key=None, region=None): def build_hosts(self): # type: () -> HostsCollection - return HostsCollection( - [Host("{}.{}.{}".format("recommendation", self._region, "algolia.com"))] - ) + return HostsCollection([Host("{}.{}.{}".format("recommendation", self._region, "algolia.com"))]) + class AnswersConfig(SearchConfig): def __init__(self, app_id=None, api_key=None): diff --git a/tests/features/test_answers_client.py b/tests/features/test_answers_client.py index 2f7915964..e466b8f87 100644 --- a/tests/features/test_answers_client.py +++ b/tests/features/test_answers_client.py @@ -9,17 +9,20 @@ def setUp(self): self.search_client = F.search_client() self.client = F.answers_client() self.index = F.index(self.search_client, self._testMethodName) - self.index.save_objects([ - { - "name": "Something", - "description": "Your usage of demo datasets is usually more creative :')", - "objectID": 0, - }, { - "name": "Another thing", - "description": "This is creative, but unused. ;)", - "objectID": 1, - }, - ]).wait() + self.index.save_objects( + [ + { + "name": "Something", + "description": "Your usage of demo datasets is usually more creative :')", + "objectID": 0, + }, + { + "name": "Another thing", + "description": "This is creative, but unused. ;)", + "objectID": 1, + }, + ] + ).wait() def tearDown(self): self.client.close() @@ -29,7 +32,7 @@ def test_answers(self): "query": "Any usage?", "queryLanguages": ["en"], "attributesForPrediction": ["title", "description"], - "nbHits": 2 + "nbHits": 2, } try: From 36025086d8395944ad5bd75151f950dc9cf46da7 Mon Sep 17 00:00:00 2001 From: Paul-Louis NECH <1821404+PLNech@users.noreply.github.com> Date: Wed, 16 Jun 2021 12:29:29 +0200 Subject: [PATCH 4/5] chore: Apply suggestions from code review Co-authored-by: TomKlotz --- algoliasearch/answers_client.py | 13 +++++++------ algoliasearch/configs.py | 8 ++++++-- tests/features/test_answers_client.py | 2 +- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/algoliasearch/answers_client.py b/algoliasearch/answers_client.py index 8f3844c7d..192040a63 100644 --- a/algoliasearch/answers_client.py +++ b/algoliasearch/answers_client.py @@ -33,17 +33,18 @@ def create_with_config(config): client = AnswersClient(transporter, config) if is_async_available(): - from algoliasearch.answers_client_async import ( - AnswersClientAsync, - ) + from algoliasearch.answers_client_async import AnswersClientAsync from algoliasearch.http.transporter_async import TransporterAsync from algoliasearch.http.requester_async import RequesterAsync - return AnswersClientAsync(client, TransporterAsync(RequesterAsync(), config), config) - + return AnswersClientAsync( + client, TransporterAsync(RequesterAsync(), config), config + ) return client - def predict(self, index_name, answers_parameters, request_options=None): # noqa: E501 + def predict( + self, index_name, answers_parameters, request_options=None + ): # type: (str, dict, Optional[Union[dict, RequestOptions]]) -> dict return self._transporter.write( diff --git a/algoliasearch/configs.py b/algoliasearch/configs.py index ed5777748..181dc99f0 100644 --- a/algoliasearch/configs.py +++ b/algoliasearch/configs.py @@ -103,7 +103,9 @@ def __init__(self, app_id=None, api_key=None, region=None): def build_hosts(self): # type: () -> HostsCollection - return HostsCollection([Host("{}.{}.{}".format("insights", self._region, "algolia.io"))]) + return HostsCollection( + [Host("{}.{}.{}".format("insights", self._region, "algolia.io"))] + ) class RecommendationConfig(Config): @@ -117,7 +119,9 @@ def __init__(self, app_id=None, api_key=None, region=None): def build_hosts(self): # type: () -> HostsCollection - return HostsCollection([Host("{}.{}.{}".format("recommendation", self._region, "algolia.com"))]) + return HostsCollection( + [Host("{}.{}.{}".format("recommendation", self._region, "algolia.com"))] + ) class AnswersConfig(SearchConfig): diff --git a/tests/features/test_answers_client.py b/tests/features/test_answers_client.py index e466b8f87..6cf2d82cc 100644 --- a/tests/features/test_answers_client.py +++ b/tests/features/test_answers_client.py @@ -13,7 +13,7 @@ def setUp(self): [ { "name": "Something", - "description": "Your usage of demo datasets is usually more creative :')", + "description": "The usage is strong in that one", "objectID": 0, }, { From 9bb32628a3fd8e017f4ab14d0e7001519f5b5197 Mon Sep 17 00:00:00 2001 From: Tom Klotz Date: Wed, 16 Jun 2021 12:46:05 +0200 Subject: [PATCH 5/5] style: use blake to fix format --- algoliasearch/answers_client.py | 9 +++++---- algoliasearch/answers_client_async.py | 4 +++- algoliasearch/configs.py | 4 +++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/algoliasearch/answers_client.py b/algoliasearch/answers_client.py index 192040a63..d443abcad 100644 --- a/algoliasearch/answers_client.py +++ b/algoliasearch/answers_client.py @@ -37,14 +37,15 @@ def create_with_config(config): from algoliasearch.http.transporter_async import TransporterAsync from algoliasearch.http.requester_async import RequesterAsync - return AnswersClientAsync( - client, TransporterAsync(RequesterAsync(), config), config - ) + return AnswersClientAsync( + client, TransporterAsync(RequesterAsync(), config), config + ) + return client def predict( self, index_name, answers_parameters, request_options=None - ): + ): # noqa: E501 # type: (str, dict, Optional[Union[dict, RequestOptions]]) -> dict return self._transporter.write( diff --git a/algoliasearch/answers_client_async.py b/algoliasearch/answers_client_async.py index 89660104c..c14d57358 100644 --- a/algoliasearch/answers_client_async.py +++ b/algoliasearch/answers_client_async.py @@ -14,7 +14,9 @@ def __init__(self, answers_client, transporter, search_config): self._transporter_async = transporter - super(AnswersClientAsync, self).__init__(answers_client._transporter, search_config) + super(AnswersClientAsync, self).__init__( + answers_client._transporter, search_config + ) client = AnswersClient(transporter, search_config) diff --git a/algoliasearch/configs.py b/algoliasearch/configs.py index 181dc99f0..fd47583fa 100644 --- a/algoliasearch/configs.py +++ b/algoliasearch/configs.py @@ -89,7 +89,9 @@ def __init__(self, app_id=None, api_key=None, region=None): def build_hosts(self): # type: () -> HostsCollection - return HostsCollection([Host("{}.{}.{}".format("analytics", self._region, "algolia.com"))]) + return HostsCollection( + [Host("{}.{}.{}".format("analytics", self._region, "algolia.com"))] + ) class InsightsConfig(Config):