Skip to content

Commit 8f0829d

Browse files
committed
Support setting the region of your DB. Added Update / Delete More Items endpoints. Drop python2 support.
Added type hints. Removed deprecated endpoints Item Based Recommendation and User Based Recommendation.
1 parent 8f124d4 commit 8f0829d

File tree

183 files changed

+742
-1083
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

183 files changed

+742
-1083
lines changed

README.rst

+5-7
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
RecombeeApiClient
33
*****************
44

5-
A Python client for easy use of the `Recombee <https://www.recombee.com/>`_ recommendation API. Both Python 2 and Python 3 are supported.
5+
A Python 3 client for easy use of the `Recombee <https://www.recombee.com/>`_ recommendation API.
66

77
If you don't have an account at Recombee yet, you can create a free account `here <https://www.recombee.com/>`_.
88

@@ -18,8 +18,6 @@ Install the client with pip:
1818
1919
$ pip install recombee-api-client
2020
21-
(use pip3 instead of pip if you use Python 3)
22-
2321
========
2422
Examples
2523
========
@@ -30,12 +28,12 @@ Basic example
3028

3129
.. code-block:: python
3230
33-
from recombee_api_client.api_client import RecombeeClient
31+
from recombee_api_client.api_client import RecombeeClient, Region
3432
from recombee_api_client.exceptions import APIException
3533
from recombee_api_client.api_requests import *
3634
import random
3735
38-
client = RecombeeClient('--my-database-id--', '--db-private-token--')
36+
client = RecombeeClient('--my-database-id--', '--db-private-token--', region=Region.US_WEST)
3937
4038
#Generate some random purchases of items by users
4139
PROBABILITY_PURCHASED = 0.1
@@ -73,15 +71,15 @@ Using property values
7371

7472
.. code-block:: python
7573
76-
from recombee_api_client.api_client import RecombeeClient
74+
from recombee_api_client.api_client import RecombeeClient, Region
7775
from recombee_api_client.api_requests import AddItemProperty, SetItemValues, AddPurchase
7876
from recombee_api_client.api_requests import RecommendItemsToItem, SearchItems, Batch, ResetDatabase
7977
import random
8078
8179
NUM = 100
8280
PROBABILITY_PURCHASED = 0.1
8381
84-
client = RecombeeClient('--my-database-id--', '--db-private-token--')
82+
client = RecombeeClient('--my-database-id--', '--db-private-token--', region=Region.AP_SE)
8583
8684
# Clear the entire database
8785
client.send(ResetDatabase())

recombee_api_client/api_client.py

+87-62
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
11
import os
22
import time
3-
import hmac
43
import json
5-
from hashlib import sha1
6-
import requests
4+
import hmac
5+
from typing import Union
6+
from enum import Enum
77

8-
try:
9-
from urllib import quote
10-
except ImportError:
11-
from urllib.parse import quote
8+
import requests
9+
from hashlib import sha1
10+
from urllib.parse import quote
1211

1312
from recombee_api_client.exceptions import ApiTimeoutException, ResponseException
14-
from recombee_api_client.api_requests import Batch
13+
from recombee_api_client.api_requests import Batch, Request
14+
15+
16+
class Region(Enum):
17+
"""
18+
Region of the Recombee cluster
19+
"""
20+
AP_SE = 1
21+
CA_EAST = 2
22+
EU_WEST = 3
23+
US_WEST = 4
24+
1525

1626
class RecombeeClient:
1727
"""
@@ -22,22 +32,19 @@ class RecombeeClient:
2232
:param token: Secret token obtained from Recombee for signing requests
2333
2434
:param protocol: Default protocol for sending requests. Possible values: 'http', 'https'.
35+
36+
:param region: region of the Recombee cluster where the database is located
2537
"""
2638
BATCH_MAX_SIZE = 10000
2739

28-
def __init__(self, database_id, token, protocol = 'https', options = {}):
40+
def __init__(self, database_id: str, token: str, protocol: str = 'https', options: dict = None, region: Region = None):
2941
self.database_id = database_id
3042
self.token = token
3143
self.protocol = protocol
3244

33-
self.base_uri = os.environ.get('RAPI_URI')
34-
if self.base_uri is None:
35-
self.base_uri = options.get('base_uri')
36-
if self.base_uri is None:
37-
self.base_uri = 'rapi.recombee.com'
38-
45+
self.base_uri = self.__get_base_uri(options=options or {}, region=region)
3946

40-
def send(self, request):
47+
def send(self, request: Request) -> Union[dict, str, list]:
4148
"""
4249
:param request: Request to be sent to Recombee recommender
4350
"""
@@ -63,101 +70,119 @@ def send(self, request):
6370
raise ApiTimeoutException(request)
6471

6572
@staticmethod
66-
def __get_http_headers(additional_headers=None):
67-
headers = {'User-Agent': 'recombee-python-api-client/3.2.0'}
73+
def __get_regional_base_uri(region: Region) -> str:
74+
uri = {
75+
Region.AP_SE: 'rapi-ap-se.recombee.com',
76+
Region.CA_EAST: 'rapi-ca-east.recombee.com',
77+
Region.EU_WEST: 'rapi-eu-west.recombee.com',
78+
Region.US_WEST: 'rapi-us-west.recombee.com'
79+
}.get(region)
80+
81+
if uri is None:
82+
raise ValueError('Unknown region given')
83+
return uri
84+
85+
@staticmethod
86+
def __get_base_uri(options: dict, region: str) -> str:
87+
base_uri = os.environ.get('RAPI_URI') or options.get('base_uri')
88+
if region is not None:
89+
if base_uri:
90+
raise ValueError('base_uri and region cannot be specified at the same time')
91+
base_uri = RecombeeClient.__get_regional_base_uri(region)
92+
93+
return base_uri or 'rapi.recombee.com'
94+
95+
@staticmethod
96+
def __get_http_headers(additional_headers: dict = None) -> dict:
97+
headers = {'User-Agent': 'recombee-python-api-client/4.0.0'}
6898
if additional_headers:
6999
headers.update(additional_headers)
70100
return headers
71101

72-
def __put(self, request, uri, timeout):
102+
def __put(self, request: Request, uri: str, timeout: int):
73103
response = requests.put(uri,
74104
data=json.dumps(request.get_body_parameters()),
75-
headers= self.__get_http_headers({'Content-Type': 'application/json'}),
105+
headers=self.__get_http_headers({'Content-Type': 'application/json'}),
76106
timeout=timeout)
77107
self.__check_errors(response, request)
78108
return response.json()
79109

80-
def __get(self, request, uri, timeout):
110+
def __get(self, request: Request, uri: str, timeout: int):
81111
response = requests.get(uri,
82-
headers= self.__get_http_headers(),
112+
headers=self.__get_http_headers(),
83113
timeout=timeout)
84114
self.__check_errors(response, request)
85115
return response.json()
86116

87-
def __post(self, request, uri, timeout):
117+
def __post(self, request: Request, uri: str, timeout: int):
88118
response = requests.post(uri,
89-
data=json.dumps(request.get_body_parameters()),
90-
headers= self.__get_http_headers({'Content-Type': 'application/json'}),
91-
timeout=timeout)
119+
data=json.dumps(request.get_body_parameters()),
120+
headers=self.__get_http_headers({'Content-Type': 'application/json'}),
121+
timeout=timeout)
92122
self.__check_errors(response, request)
93123
return response.json()
94124

95-
def __delete(self, request, uri, timeout):
125+
def __delete(self, request: Request, uri: str, timeout: int):
96126
response = requests.delete(uri,
97-
headers= self.__get_http_headers(),
98-
timeout=timeout)
127+
data=json.dumps(request.get_body_parameters()),
128+
headers=self.__get_http_headers({'Content-Type': 'application/json'}),
129+
timeout=timeout)
99130
self.__check_errors(response, request)
100131
return response.json()
101132

102-
103-
def __check_errors(self, response, request):
133+
def __check_errors(self, response, request: Request):
104134
status_code = response.status_code
105135
if status_code == 200 or status_code == 201:
106136
return
107137
raise ResponseException(request, status_code, response.text)
108138

109139
@staticmethod
110-
def __get_list_chunks(l, n):
140+
def __get_list_chunks(l: list, n: int) -> list:
111141
"""Yield successive n-sized chunks from l."""
112142

113-
try: #Python 2/3 compatibility
114-
xrange
115-
except NameError:
116-
xrange = range
117-
118-
for i in xrange(0, len(l), n):
143+
for i in range(0, len(l), n):
119144
yield l[i:i + n]
120145

121-
def __send_multipart_batch(self, batch):
146+
def __send_multipart_batch(self, batch: Batch) -> list:
122147
requests_parts = [rqs for rqs in self.__get_list_chunks(batch.requests, self.BATCH_MAX_SIZE)]
123148
responses = [self.send(Batch(rqs)) for rqs in requests_parts]
124149
return sum(responses, [])
125150

126-
def __process_request_uri(self, request):
151+
def __process_request_uri(self, request: Request) -> str:
127152
uri = request.path
128153
uri += self.__query_parameters_to_url(request)
129154
return uri
130155

156+
def __query_parameters_to_url(self, request: Request) -> str:
157+
ps = ''
158+
query_params = request.get_query_parameters()
159+
for name in query_params:
160+
val = query_params[name]
161+
ps += '&' if ps.find('?') != -1 else '?'
162+
ps += "%s=%s" % (name, self.__format_query_parameter_value(val))
163+
return ps
131164

132-
def __query_parameters_to_url(self, request):
133-
ps = ''
134-
query_params = request.get_query_parameters()
135-
for name in query_params:
136-
val = query_params[name]
137-
ps += '&' if ps.find('?')!=-1 else '?'
138-
ps += "%s=%s" % (name, self.__format_query_parameter_value(val))
139-
return ps
140-
141-
def __format_query_parameter_value(self, value):
142-
if isinstance(value, list):
143-
return ','.join([quote(str(v)) for v in value])
144-
return quote(str(value))
165+
@staticmethod
166+
def __format_query_parameter_value(value) -> str:
167+
if isinstance(value, list):
168+
return ','.join([quote(str(v)) for v in value])
169+
return quote(str(value))
145170

146-
# Sign request with HMAC, request URI must be exacly the same
171+
# Sign request with HMAC, request URI must be exactly the same
147172
# We have 30s to complete request with this token
148-
def __sign_url(self, req_part):
173+
def __sign_url(self, req_part: str) -> str:
149174
uri = '/' + self.database_id + req_part
150-
time = self.__hmac_time(uri)
151-
sign = self.__hmac_sign(uri, time)
152-
res = uri + time + '&hmac_sign=' +sign
175+
time_part = self.__hmac_time(uri)
176+
sign = self.__hmac_sign(uri, time_part)
177+
res = uri + time_part + '&hmac_sign=' + sign
153178
return res
154179

155-
def __hmac_time(self, uri):
156-
res = '&' if uri.find('?')!=-1 else '?'
180+
def __hmac_time(self, uri: str) -> str:
181+
res = '&' if uri.find('?') != -1 else '?'
157182
res += "hmac_timestamp=%s" % int(time.time())
158183
return res
159184

160-
def __hmac_sign(self, uri, time):
161-
url = uri + time
185+
def __hmac_sign(self, uri: str, time_part: str) -> str:
186+
url = uri + time_part
162187
sign = hmac.new(str.encode(self.token), str.encode(url), sha1).hexdigest()
163188
return sign

recombee_api_client/api_requests/__init__.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from recombee_api_client.api_requests.delete_item_property import DeleteItemProperty
88
from recombee_api_client.api_requests.get_item_property_info import GetItemPropertyInfo
99
from recombee_api_client.api_requests.list_item_properties import ListItemProperties
10+
from recombee_api_client.api_requests.update_more_items import UpdateMoreItems
11+
from recombee_api_client.api_requests.delete_more_items import DeleteMoreItems
1012
from recombee_api_client.api_requests.add_series import AddSeries
1113
from recombee_api_client.api_requests.delete_series import DeleteSeries
1214
from recombee_api_client.api_requests.list_series import ListSeries
@@ -58,12 +60,11 @@
5860
from recombee_api_client.api_requests.recommend_next_items import RecommendNextItems
5961
from recombee_api_client.api_requests.recommend_users_to_user import RecommendUsersToUser
6062
from recombee_api_client.api_requests.recommend_users_to_item import RecommendUsersToItem
61-
from recombee_api_client.api_requests.user_based_recommendation import UserBasedRecommendation
62-
from recombee_api_client.api_requests.item_based_recommendation import ItemBasedRecommendation
6363
from recombee_api_client.api_requests.search_items import SearchItems
6464
from recombee_api_client.api_requests.add_search_synonym import AddSearchSynonym
6565
from recombee_api_client.api_requests.list_search_synonyms import ListSearchSynonyms
6666
from recombee_api_client.api_requests.delete_all_search_synonyms import DeleteAllSearchSynonyms
6767
from recombee_api_client.api_requests.delete_search_synonym import DeleteSearchSynonym
6868
from recombee_api_client.api_requests.reset_database import ResetDatabase
6969
from recombee_api_client.api_requests.batch import Batch
70+
from recombee_api_client.api_requests.request import Request

recombee_api_client/api_requests/add_bookmark.py

+5-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from recombee_api_client.api_requests.request import Request
2+
from typing import Union, List
23
import uuid
34

45
DEFAULT = uuid.uuid4()
@@ -27,19 +28,16 @@ class AddBookmark(Request):
2728
2829
"""
2930

30-
def __init__(self, user_id, item_id, timestamp=DEFAULT, cascade_create=DEFAULT, recomm_id=DEFAULT, additional_data=DEFAULT):
31+
def __init__(self, user_id: str, item_id: str, timestamp: Union[str, int] = DEFAULT, cascade_create: bool = DEFAULT, recomm_id: str = DEFAULT, additional_data: dict = DEFAULT):
32+
super().__init__(path="/bookmarks/" % (), method='post', timeout=1000, ensure_https=False)
3133
self.user_id = user_id
3234
self.item_id = item_id
3335
self.timestamp = timestamp
3436
self.cascade_create = cascade_create
3537
self.recomm_id = recomm_id
3638
self.additional_data = additional_data
37-
self.timeout = 1000
38-
self.ensure_https = False
39-
self.method = 'post'
40-
self.path = "/bookmarks/" % ()
4139

42-
def get_body_parameters(self):
40+
def get_body_parameters(self) -> dict:
4341
"""
4442
Values of body parameters as a dictionary (name of parameter: value of the parameter).
4543
"""
@@ -56,7 +54,7 @@ def get_body_parameters(self):
5654
p['additionalData'] = self.additional_data
5755
return p
5856

59-
def get_query_parameters(self):
57+
def get_query_parameters(self) -> dict:
6058
"""
6159
Values of query parameters as a dictionary (name of parameter: value of the parameter).
6260
"""

recombee_api_client/api_requests/add_cart_addition.py

+5-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from recombee_api_client.api_requests.request import Request
2+
from typing import Union, List
23
import uuid
34

45
DEFAULT = uuid.uuid4()
@@ -31,7 +32,8 @@ class AddCartAddition(Request):
3132
3233
"""
3334

34-
def __init__(self, user_id, item_id, timestamp=DEFAULT, cascade_create=DEFAULT, amount=DEFAULT, price=DEFAULT, recomm_id=DEFAULT, additional_data=DEFAULT):
35+
def __init__(self, user_id: str, item_id: str, timestamp: Union[str, int] = DEFAULT, cascade_create: bool = DEFAULT, amount: float = DEFAULT, price: float = DEFAULT, recomm_id: str = DEFAULT, additional_data: dict = DEFAULT):
36+
super().__init__(path="/cartadditions/" % (), method='post', timeout=1000, ensure_https=False)
3537
self.user_id = user_id
3638
self.item_id = item_id
3739
self.timestamp = timestamp
@@ -40,12 +42,8 @@ def __init__(self, user_id, item_id, timestamp=DEFAULT, cascade_create=DEFAULT,
4042
self.price = price
4143
self.recomm_id = recomm_id
4244
self.additional_data = additional_data
43-
self.timeout = 1000
44-
self.ensure_https = False
45-
self.method = 'post'
46-
self.path = "/cartadditions/" % ()
4745

48-
def get_body_parameters(self):
46+
def get_body_parameters(self) -> dict:
4947
"""
5048
Values of body parameters as a dictionary (name of parameter: value of the parameter).
5149
"""
@@ -66,7 +64,7 @@ def get_body_parameters(self):
6664
p['additionalData'] = self.additional_data
6765
return p
6866

69-
def get_query_parameters(self):
67+
def get_query_parameters(self) -> dict:
7068
"""
7169
Values of query parameters as a dictionary (name of parameter: value of the parameter).
7270
"""

0 commit comments

Comments
 (0)