From dc716c0a367fd6378cc0b14b1b9044e6005c22d6 Mon Sep 17 00:00:00 2001 From: zhenghaoz Date: Sun, 27 Nov 2022 18:58:38 +0800 Subject: [PATCH] implement async client (#2) --- .github/workflows/ci.yml | 2 +- README.md | 17 ++ gorse/__init__.py | 143 ++++++++++++++ setup.py | 2 +- tests/test_gorse.py | 402 ++++++++++++++++++++++++++------------- 5 files changed, 434 insertions(+), 132 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73d3438..41ea26b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: name: test (Python ${{ matrix.python-version }}) strategy: matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v2 - name: Build the stack diff --git a/README.md b/README.md index 95bcff0..30d752b 100644 --- a/README.md +++ b/README.md @@ -40,3 +40,20 @@ client.insert_feedbacks([ client.get_recommend('bob', n=10) ``` + +The Python SDK implements the async client as well: + +```python +from gorse import AsyncGorse + +client = AsyncGorse('http://127.0.0.1:8087', 'api_key') +await client.insert_feedbacks([ + { 'FeedbackType': 'star', 'UserId': 'bob', 'ItemId': 'vuejs:vue', 'Timestamp': '2022-02-24' }, + { 'FeedbackType': 'star', 'UserId': 'bob', 'ItemId': 'd3:d3', 'Timestamp': '2022-02-25' }, + { 'FeedbackType': 'star', 'UserId': 'bob', 'ItemId': 'dogfalo:materialize', 'Timestamp': '2022-02-26' }, + { 'FeedbackType': 'star', 'UserId': 'bob', 'ItemId': 'mozilla:pdf.js', 'Timestamp': '2022-02-27' }, + { 'FeedbackType': 'star', 'UserId': 'bob', 'ItemId': 'moment:moment', 'Timestamp': '2022-02-28' } +]) + +await client.get_recommend('bob', n=10) +``` diff --git a/gorse/__init__.py b/gorse/__init__.py index 25c2eab..d6ed0d8 100644 --- a/gorse/__init__.py +++ b/gorse/__init__.py @@ -13,16 +13,25 @@ # limitations under the License. from typing import List, Tuple +import aiohttp import requests class GorseException(Exception): + """ + Gorse exception. + """ + def __init__(self, status_code: int, message: str): self.status_code = status_code self.message = message class Gorse: + """ + Gorse client. + """ + def __init__(self, entry_point: str, api_key: str, timeout=None): self.entry_point = entry_point self.api_key = api_key @@ -155,3 +164,137 @@ def __request(self, method: str, url: str, params=None, json=None) -> dict: if response.status_code == 200: return response.json() raise GorseException(response.status_code, response.text) + + +class AsyncGorse: + """ + Gorse async client. + """ + + def __init__(self, entry_point: str, api_key: str, timeout=None): + self.entry_point = entry_point + self.api_key = api_key + self.timeout = timeout + + async def insert_feedback( + self, feedback_type: str, user_id: str, item_id: str, timestamp: str + ) -> dict: + """ + Insert a feedback. + """ + return await self.__request( + "POST", + f"{self.entry_point}/api/feedback", + json=[ + { + "FeedbackType": feedback_type, + "UserId": user_id, + "ItemId": item_id, + "Timestamp": timestamp, + } + ], + ) + + async def list_feedbacks(self, feedback_type: str, user_id: str): + """ + List feedbacks from a user. + """ + return await self.__request("GET", f"{self.entry_point}/api/user/{user_id}/feedback/{feedback_type}") + + async def get_recommend(self, user_id: str, category: str = "", n: int = 10, offset: int = 0, write_back_type: str = None, + write_back_delay: str = None) -> List[str]: + """ + Get recommendation. + """ + payload = {"n": n, "offset": offset} + if write_back_type: + payload["write-back-type"] = write_back_type + if write_back_delay: + payload["write-back-delay"] = write_back_delay + return await self.__request("GET", f"{self.entry_point}/api/recommend/{user_id}/{category}", params=payload) + + async def session_recommend(self, feedbacks: list, n: int = 10) -> list: + """ + Get session recommendation. + """ + return await self.__request("POST", f"{self.entry_point}/api/session/recommend?n={n}", json=feedbacks) + + async def get_neighbors(self, item_id: str, n: int = 10, offset: int = 0) -> List[str]: + """ + Get item neighbors. + """ + return await self.__request("GET", f"{self.entry_point}/api/item/{item_id}/neighbors?n={n}&offset={offset}") + + async def insert_feedbacks(self, feedbacks: list) -> dict: + """ + Insert feedbacks. + """ + return await self.__request("POST", f"{self.entry_point}/api/feedback", json=feedbacks) + + async def insert_item(self, item) -> dict: + """ + Insert an item. + """ + return await self.__request("POST", f"{self.entry_point}/api/item", json=item) + + async def get_item(self, item_id: str) -> dict: + """ + Get an item. + """ + return await self.__request("GET", f"{self.entry_point}/api/item/{item_id}") + + async def get_items(self, n: int, cursor: str = '') -> Tuple[List[dict], str]: + """ + Get items. + :param n: number of returned items + :param cursor: cursor for next page + :return: items and cursor for next page + """ + response = await self.__request( + "GET", f"{self.entry_point}/api/items", params={'n': n, 'cursor': cursor}) + return response['Items'], response['Cursor'] + + async def update_item(self, item_id: str, is_hidden: bool = None, categories: List[str] = None, labels: List[str] = None, + timestamp: str = None, + comment: str = None) -> dict: + """ + Update an item. + """ + return await self.__request("PATCH", f'{self.entry_point}/api/item/{item_id}', json={ + "Categories": categories, + "Comment": comment, + "IsHidden": is_hidden, + "Labels": labels, + "Timestamp": timestamp + }) + + async def delete_item(self, item_id: str) -> dict: + """ + Delete an item. + """ + return await self.__request("DELETE", f"{self.entry_point}/api/item/{item_id}") + + async def insert_user(self, user) -> dict: + """ + Insert a user. + """ + return await self.__request("POST", f"{self.entry_point}/api/user", json=user) + + async def get_user(self, user_id: str) -> dict: + """ + Get a user. + """ + return await self.__request("GET", f"{self.entry_point}/api/user/{user_id}") + + async def delete_user(self, user_id: str) -> dict: + """ + Delete a user. + """ + return await self.__request("DELETE", f"{self.entry_point}/api/user/{user_id}") + + async def __request(self, method: str, url: str, params=None, json=None) -> dict: + async with aiohttp.ClientSession() as session: + async with session.request(method, url, params=params, json=json, headers={"X-API-Key": self.api_key}) as response: + if response.status == 200: + return await response.json() + raise GorseException(response.status, await response.text()) diff --git a/setup.py b/setup.py index 697ee08..55ac74b 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ version='0.4.6', description='Python SDK for gorse recommender system', packages=['gorse'], - install_requires=['requests>=2.14.0'], + install_requires=['requests~=2.14.0', 'aiohttp~=3.8.3'], long_description=long_description, long_description_content_type='text/markdown' ) diff --git a/tests/test_gorse.py b/tests/test_gorse.py index b0190ef..e7a26ce 100644 --- a/tests/test_gorse.py +++ b/tests/test_gorse.py @@ -12,141 +12,283 @@ # See the License for the specific language governing permissions and # limitations under the License. from datetime import datetime +import unittest import redis -from gorse import Gorse, GorseException +from gorse import Gorse, GorseException, AsyncGorse GORSE_ENDPOINT = 'http://127.0.0.1:8088' GORSE_API_KEY = 'zhenghaoz' -def test_users(): - client = Gorse(GORSE_ENDPOINT, GORSE_API_KEY) - # Insert a user. - r = client.insert_user({'UserId': '100', 'Labels': ['a', 'b', 'c'], 'Subscribe': ['d', 'e'], 'Comment': 'comment'}) - assert r['RowAffected'] == 1 - # Get this user. - user = client.get_user('100') - assert user == {'UserId': '100', 'Labels': ['a', 'b', 'c'], 'Subscribe': ['d', 'e'], 'Comment': 'comment'} - # Delete this user. - r = client.delete_user('100') - assert r['RowAffected'] == 1 - try: - client.get_user('100') - assert False - except GorseException as e: - assert e.status_code == 404 - - -def test_items(): - client = Gorse(GORSE_ENDPOINT, GORSE_API_KEY) - timestamp = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') - items = [ - {'ItemId': '100', 'IsHidden': True, 'Labels': ['a', 'b', 'c'], 'Categories': ['d', 'e'], 'Timestamp': timestamp, - 'Comment': 'comment'}, - {'ItemId': '200', 'IsHidden': True, 'Labels': ['b', 'c', 'd'], 'Categories': ['d', 'a'], 'Timestamp': timestamp, - 'Comment': 'comment'}, - {'ItemId': '300', 'IsHidden': True, 'Labels': ['c', 'd', 'e'], 'Categories': ['d', 'j'], 'Timestamp': timestamp, - 'Comment': 'comment'}, - {'ItemId': '400', 'IsHidden': True, 'Labels': ['d', 'e', 'f'], 'Categories': ['d', 'm'], 'Timestamp': timestamp, - 'Comment': 'comment'}, - {'ItemId': '500', 'IsHidden': True, 'Labels': ['e', 'f', 'g'], 'Categories': ['d', 't'], 'Timestamp': timestamp, - 'Comment': 'comment'} - ] - # Insert items. - for item in items: - r = client.insert_item(item) - assert r['RowAffected'] == 1 - # Get an item. - item = client.get_item('100') - assert item == items[0] - # Get items - return_items = [] - part, cursor = client.get_items(3) - assert len(part) == 3 - assert len(cursor) > 0 - return_items.extend(part) - part, cursor = client.get_items(3, cursor) - assert len(part) == 2 - assert len(cursor) == 0 - return_items.extend(part) - assert return_items == items - # Update item - r = client.update_item('100', labels=['x', 'y', 'z']) - assert r['RowAffected'] == 1 - item = client.get_item('100') - assert item == {'ItemId': '100', 'IsHidden': True, 'Labels': ['x', 'y', 'z'], 'Categories': ['d', 'e'], - 'Timestamp': timestamp, - 'Comment': 'comment'} - # Delete this item. - r = client.delete_item('100') - assert r['RowAffected'] == 1 - try: - client.get_item('100') - assert False - except GorseException as e: - assert e.status_code == 404 - - -def test_feedback(): - client = Gorse(GORSE_ENDPOINT, GORSE_API_KEY) - timestamp = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') - # Insert a feedback - r = client.insert_feedback('like', '100', '100', timestamp) - assert r['RowAffected'] == 1 - # Insert feedbacks - r = client.insert_feedbacks([ - {'FeedbackType': 'read', 'UserId': '100', 'ItemId': '200', 'Timestamp': timestamp}, - {'FeedbackType': 'read', 'UserId': '100', 'ItemId': '300', 'Timestamp': timestamp} - ]) - assert r['RowAffected'] == 2 - # List feedbacks - feedbacks = client.list_feedbacks('read', '100') - assert feedbacks == [ - {'FeedbackType': 'read', 'UserId': '100', 'ItemId': '200', 'Timestamp': timestamp, 'Comment': ''}, - {'FeedbackType': 'read', 'UserId': '100', 'ItemId': '300', 'Timestamp': timestamp, 'Comment': ''} - ] - - -def test_recommend(): - r = redis.Redis(host='127.0.0.1', port=6379, db=0) - r.zadd('offline_recommend/100', {'1': 1, '2': 2, '3': 3}) - - client = Gorse(GORSE_ENDPOINT, GORSE_API_KEY) - recommend = client.get_recommend('100') - assert recommend == ['3', '2', '1'] - recommend = client.get_recommend("100", n=1, offset=1) - assert recommend == ["2"] - - -def test_neighbors(): - r = redis.Redis(host='127.0.0.1', port=6379, db=0) - r.zadd('item_neighbors/100', {'1': 1, '2': 2, '3': 3}) - - client = Gorse(GORSE_ENDPOINT, GORSE_API_KEY) - items = client.get_neighbors('100', n=3) - assert items == [{'Id': '3', 'Score': 3}, {'Id': '2', 'Score': 2}, {'Id': '1', 'Score': 1}] - items = client.get_neighbors('100', n=1, offset=1) - assert items == [{'Id': '2', 'Score': 2}] - - -def test_session_recommend(): - r = redis.Redis(host='127.0.0.1', port=6379, db=0) - r.zadd('item_neighbors/1', {"2": 100000, "9": 1}) - r.zadd('item_neighbors/2', {"3": 100000, "8": 1, "9": 1}) - r.zadd('item_neighbors/3', {"4": 100000, "7": 1, "8": 1, "9": 1}) - r.zadd('item_neighbors/4', {"1": 100000, "6": 1, "7": 1, "8": 1, "9": 1}) - - client = Gorse(GORSE_ENDPOINT, GORSE_API_KEY) - recommend = client.session_recommend([ - {"FeedbackType": "like", "UserId": "0", "ItemId": "1", - "Timestamp": datetime(2010, 1, 1, 1, 1, 1, 1).isoformat()}, - {"FeedbackType": "like", "UserId": "0", "ItemId": "2", - "Timestamp": datetime(2009, 1, 1, 1, 1, 1, 1).isoformat()}, - {"FeedbackType": "like", "UserId": "0", "ItemId": "3", - "Timestamp": datetime(2008, 1, 1, 1, 1, 1, 1).isoformat()}, - {"FeedbackType": "like", "UserId": "0", "ItemId": "4", - "Timestamp": datetime(2007, 1, 1, 1, 1, 1, 1).isoformat()}, - ], 3) - assert recommend == [{'Id': "9", 'Score': 4}, {'Id': "8", 'Score': 3}, {'Id': "7", 'Score': 2}] +class TestGorseClient(unittest.TestCase): + def test_users(self): + client = Gorse(GORSE_ENDPOINT, GORSE_API_KEY) + # Insert a user. + r = client.insert_user({'UserId': '100', 'Labels': [ + 'a', 'b', 'c'], 'Subscribe': ['d', 'e'], 'Comment': 'comment'}) + self.assertEqual(r['RowAffected'], 1) + # Get this user. + user = client.get_user('100') + self.assertDictEqual(user, {'UserId': '100', 'Labels': [ + 'a', 'b', 'c'], 'Subscribe': ['d', 'e'], 'Comment': 'comment'}) + # Delete this user. + r = client.delete_user('100') + self.assertEqual(r['RowAffected'], 1) + try: + client.get_user('100') + self.fail() + except GorseException as e: + self.assertEqual(e.status_code, 404) + + def test_items(self): + client = Gorse(GORSE_ENDPOINT, GORSE_API_KEY) + timestamp = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') + items = [ + {'ItemId': '100', 'IsHidden': True, 'Labels': ['a', 'b', 'c'], 'Categories': ['d', 'e'], 'Timestamp': timestamp, + 'Comment': 'comment'}, + {'ItemId': '200', 'IsHidden': True, 'Labels': ['b', 'c', 'd'], 'Categories': ['d', 'a'], 'Timestamp': timestamp, + 'Comment': 'comment'}, + {'ItemId': '300', 'IsHidden': True, 'Labels': ['c', 'd', 'e'], 'Categories': ['d', 'j'], 'Timestamp': timestamp, + 'Comment': 'comment'}, + {'ItemId': '400', 'IsHidden': True, 'Labels': ['d', 'e', 'f'], 'Categories': ['d', 'm'], 'Timestamp': timestamp, + 'Comment': 'comment'}, + {'ItemId': '500', 'IsHidden': True, 'Labels': ['e', 'f', 'g'], 'Categories': ['d', 't'], 'Timestamp': timestamp, + 'Comment': 'comment'} + ] + # Insert items. + for item in items: + r = client.insert_item(item) + self.assertEqual(r['RowAffected'], 1) + # Get an item. + item = client.get_item('100') + self.assertEqual(item, items[0]) + # Get items + return_items = [] + part, cursor = client.get_items(3) + self.assertEqual(len(part), 3) + self.assertGreater(len(cursor), 0) + return_items.extend(part) + part, cursor = client.get_items(3, cursor) + self.assertEqual(len(part), 2) + self.assertEqual(len(cursor), 0) + return_items.extend(part) + self.assertEqual(return_items, items) + # Update item + r = client.update_item('100', labels=['x', 'y', 'z']) + self.assertEqual(r['RowAffected'], 1) + item = client.get_item('100') + self.assertDictEqual(item, {'ItemId': '100', 'IsHidden': True, 'Labels': ['x', 'y', 'z'], 'Categories': ['d', 'e'], + 'Timestamp': timestamp, + 'Comment': 'comment'}) + # Delete this item. + r = client.delete_item('100') + self.assertEqual(r['RowAffected'], 1) + try: + client.get_item('100') + self.fail() + except GorseException as e: + self.assertEqual(e.status_code, 404) + + def test_feedback(self): + client = Gorse(GORSE_ENDPOINT, GORSE_API_KEY) + timestamp = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') + # Insert a feedback + r = client.insert_feedback('like', '100', '100', timestamp) + self.assertEqual(r['RowAffected'], 1) + # Insert feedbacks + r = client.insert_feedbacks([ + {'FeedbackType': 'read', 'UserId': '100', + 'ItemId': '200', 'Timestamp': timestamp}, + {'FeedbackType': 'read', 'UserId': '100', + 'ItemId': '300', 'Timestamp': timestamp} + ]) + self.assertEqual(r['RowAffected'], 2) + # List feedbacks + feedbacks = client.list_feedbacks('read', '100') + self.assertEqual(feedbacks, [ + {'FeedbackType': 'read', 'UserId': '100', 'ItemId': '200', + 'Timestamp': timestamp, 'Comment': ''}, + {'FeedbackType': 'read', 'UserId': '100', 'ItemId': '300', + 'Timestamp': timestamp, 'Comment': ''} + ]) + + def test_recommend(self): + r = redis.Redis(host='127.0.0.1', port=6379, db=0) + r.zadd('offline_recommend/100', {'1': 1, '2': 2, '3': 3}) + + client = Gorse(GORSE_ENDPOINT, GORSE_API_KEY) + recommend = client.get_recommend('100') + self.assertEqual(recommend, ['3', '2', '1']) + recommend = client.get_recommend("100", n=1, offset=1) + self.assertEqual(recommend, ["2"]) + + def test_neighbors(self): + r = redis.Redis(host='127.0.0.1', port=6379, db=0) + r.zadd('item_neighbors/100', {'1': 1, '2': 2, '3': 3}) + + client = Gorse(GORSE_ENDPOINT, GORSE_API_KEY) + items = client.get_neighbors('100', n=3) + self.assertEqual(items, [{'Id': '3', 'Score': 3}, { + 'Id': '2', 'Score': 2}, {'Id': '1', 'Score': 1}]) + items = client.get_neighbors('100', n=1, offset=1) + self.assertEqual(items, [{'Id': '2', 'Score': 2}]) + + def test_session_recommend(self): + r = redis.Redis(host='127.0.0.1', port=6379, db=0) + r.zadd('item_neighbors/1', {"2": 100000, "9": 1}) + r.zadd('item_neighbors/2', {"3": 100000, "8": 1, "9": 1}) + r.zadd('item_neighbors/3', {"4": 100000, "7": 1, "8": 1, "9": 1}) + r.zadd('item_neighbors/4', {"1": 100000, + "6": 1, "7": 1, "8": 1, "9": 1}) + + client = Gorse(GORSE_ENDPOINT, GORSE_API_KEY) + recommend = client.session_recommend([ + {"FeedbackType": "like", "UserId": "0", "ItemId": "1", + "Timestamp": datetime(2010, 1, 1, 1, 1, 1, 1).isoformat()}, + {"FeedbackType": "like", "UserId": "0", "ItemId": "2", + "Timestamp": datetime(2009, 1, 1, 1, 1, 1, 1).isoformat()}, + {"FeedbackType": "like", "UserId": "0", "ItemId": "3", + "Timestamp": datetime(2008, 1, 1, 1, 1, 1, 1).isoformat()}, + {"FeedbackType": "like", "UserId": "0", "ItemId": "4", + "Timestamp": datetime(2007, 1, 1, 1, 1, 1, 1).isoformat()}, + ], 3) + self.assertEqual(recommend, [{'Id': "9", 'Score': 4}, { + 'Id': "8", 'Score': 3}, {'Id': "7", 'Score': 2}]) + + +class TestAsyncGorseClient(unittest.IsolatedAsyncioTestCase): + async def test_users(self): + client = AsyncGorse(GORSE_ENDPOINT, GORSE_API_KEY) + # Insert a user. + r = await client.insert_user({'UserId': '100', 'Labels': [ + 'a', 'b', 'c'], 'Subscribe': ['d', 'e'], 'Comment': 'comment'}) + self.assertEqual(r['RowAffected'], 1) + # Get this user. + user = await client.get_user('100') + self.assertDictEqual(user, {'UserId': '100', 'Labels': [ + 'a', 'b', 'c'], 'Subscribe': ['d', 'e'], 'Comment': 'comment'}) + # Delete this user. + r = await client.delete_user('100') + self.assertEqual(r['RowAffected'], 1) + try: + await client.get_user('100') + self.fail() + except GorseException as e: + self.assertEqual(e.status_code, 404) + + async def test_items(self): + client = AsyncGorse(GORSE_ENDPOINT, GORSE_API_KEY) + timestamp = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') + items = [ + {'ItemId': '100', 'IsHidden': True, 'Labels': ['a', 'b', 'c'], 'Categories': ['d', 'e'], 'Timestamp': timestamp, + 'Comment': 'comment'}, + {'ItemId': '200', 'IsHidden': True, 'Labels': ['b', 'c', 'd'], 'Categories': ['d', 'a'], 'Timestamp': timestamp, + 'Comment': 'comment'}, + {'ItemId': '300', 'IsHidden': True, 'Labels': ['c', 'd', 'e'], 'Categories': ['d', 'j'], 'Timestamp': timestamp, + 'Comment': 'comment'}, + {'ItemId': '400', 'IsHidden': True, 'Labels': ['d', 'e', 'f'], 'Categories': ['d', 'm'], 'Timestamp': timestamp, + 'Comment': 'comment'}, + {'ItemId': '500', 'IsHidden': True, 'Labels': ['e', 'f', 'g'], 'Categories': ['d', 't'], 'Timestamp': timestamp, + 'Comment': 'comment'} + ] + # Insert items. + for item in items: + r = await client.insert_item(item) + self.assertEqual(r['RowAffected'], 1) + # Get an item. + item = await client.get_item('100') + self.assertEqual(item, items[0]) + # Get items + return_items = [] + part, cursor = await client.get_items(3) + self.assertEqual(len(part), 3) + self.assertGreater(len(cursor), 0) + return_items.extend(part) + part, cursor = await client.get_items(3, cursor) + self.assertEqual(len(part), 2) + self.assertEqual(len(cursor), 0) + return_items.extend(part) + self.assertEqual(return_items, items) + # Update item + r = await client.update_item('100', labels=['x', 'y', 'z']) + self.assertEqual(r['RowAffected'], 1) + item = await client.get_item('100') + self.assertDictEqual(item, {'ItemId': '100', 'IsHidden': True, 'Labels': ['x', 'y', 'z'], 'Categories': ['d', 'e'], + 'Timestamp': timestamp, + 'Comment': 'comment'}) + # Delete this item. + r = await client.delete_item('100') + self.assertEqual(r['RowAffected'], 1) + try: + await client.get_item('100') + self.fail() + except GorseException as e: + self.assertEqual(e.status_code, 404) + + async def test_feedback(self): + client = AsyncGorse(GORSE_ENDPOINT, GORSE_API_KEY) + timestamp = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') + # Insert a feedback + r = await client.insert_feedback('like', '100', '100', timestamp) + self.assertEqual(r['RowAffected'], 1) + # Insert feedbacks + r = await client.insert_feedbacks([ + {'FeedbackType': 'read', 'UserId': '100', + 'ItemId': '200', 'Timestamp': timestamp}, + {'FeedbackType': 'read', 'UserId': '100', + 'ItemId': '300', 'Timestamp': timestamp} + ]) + self.assertEqual(r['RowAffected'], 2) + # List feedbacks + feedbacks = await client.list_feedbacks('read', '100') + self.assertEqual(feedbacks, [ + {'FeedbackType': 'read', 'UserId': '100', 'ItemId': '200', + 'Timestamp': timestamp, 'Comment': ''}, + {'FeedbackType': 'read', 'UserId': '100', 'ItemId': '300', + 'Timestamp': timestamp, 'Comment': ''} + ]) + + async def test_recommend(self): + r = redis.Redis(host='127.0.0.1', port=6379, db=0) + r.zadd('offline_recommend/100', {'1': 1, '2': 2, '3': 3}) + + client = AsyncGorse(GORSE_ENDPOINT, GORSE_API_KEY) + recommend = await client.get_recommend('100') + self.assertEqual(recommend, ['3', '2', '1']) + recommend = await client.get_recommend("100", n=1, offset=1) + self.assertEqual(recommend, ["2"]) + + async def test_neighbors(self): + r = redis.Redis(host='127.0.0.1', port=6379, db=0) + r.zadd('item_neighbors/100', {'1': 1, '2': 2, '3': 3}) + + client = AsyncGorse(GORSE_ENDPOINT, GORSE_API_KEY) + items = await client.get_neighbors('100', n=3) + self.assertEqual(items, [{'Id': '3', 'Score': 3}, { + 'Id': '2', 'Score': 2}, {'Id': '1', 'Score': 1}]) + items = await client.get_neighbors('100', n=1, offset=1) + self.assertEqual(items, [{'Id': '2', 'Score': 2}]) + + async def test_session_recommend(self): + r = redis.Redis(host='127.0.0.1', port=6379, db=0) + r.zadd('item_neighbors/1', {"2": 100000, "9": 1}) + r.zadd('item_neighbors/2', {"3": 100000, "8": 1, "9": 1}) + r.zadd('item_neighbors/3', {"4": 100000, "7": 1, "8": 1, "9": 1}) + r.zadd('item_neighbors/4', {"1": 100000, + "6": 1, "7": 1, "8": 1, "9": 1}) + + client = AsyncGorse(GORSE_ENDPOINT, GORSE_API_KEY) + recommend = await client.session_recommend([ + {"FeedbackType": "like", "UserId": "0", "ItemId": "1", + "Timestamp": datetime(2010, 1, 1, 1, 1, 1, 1).isoformat()}, + {"FeedbackType": "like", "UserId": "0", "ItemId": "2", + "Timestamp": datetime(2009, 1, 1, 1, 1, 1, 1).isoformat()}, + {"FeedbackType": "like", "UserId": "0", "ItemId": "3", + "Timestamp": datetime(2008, 1, 1, 1, 1, 1, 1).isoformat()}, + {"FeedbackType": "like", "UserId": "0", "ItemId": "4", + "Timestamp": datetime(2007, 1, 1, 1, 1, 1, 1).isoformat()}, + ], 3) + self.assertEqual(recommend, [{'Id': "9", 'Score': 4}, { + 'Id': "8", 'Score': 3}, {'Id': "7", 'Score': 2}])