Skip to content

Commit

Permalink
Support setting the region of your DB. Added Update / Delete More Ite…
Browse files Browse the repository at this point in the history
…ms endpoints. Drop python2 support.

Added type hints.

Removed deprecated endpoints Item Based Recommendation and User Based Recommendation.
  • Loading branch information
OndraFiedler committed Apr 20, 2022
1 parent 8f124d4 commit 8f0829d
Show file tree
Hide file tree
Showing 183 changed files with 742 additions and 1,083 deletions.
12 changes: 5 additions & 7 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
RecombeeApiClient
*****************

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

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

Expand All @@ -18,8 +18,6 @@ Install the client with pip:
$ pip install recombee-api-client
(use pip3 instead of pip if you use Python 3)

========
Examples
========
Expand All @@ -30,12 +28,12 @@ Basic example

.. code-block:: python
from recombee_api_client.api_client import RecombeeClient
from recombee_api_client.api_client import RecombeeClient, Region
from recombee_api_client.exceptions import APIException
from recombee_api_client.api_requests import *
import random
client = RecombeeClient('--my-database-id--', '--db-private-token--')
client = RecombeeClient('--my-database-id--', '--db-private-token--', region=Region.US_WEST)
#Generate some random purchases of items by users
PROBABILITY_PURCHASED = 0.1
Expand Down Expand Up @@ -73,15 +71,15 @@ Using property values

.. code-block:: python
from recombee_api_client.api_client import RecombeeClient
from recombee_api_client.api_client import RecombeeClient, Region
from recombee_api_client.api_requests import AddItemProperty, SetItemValues, AddPurchase
from recombee_api_client.api_requests import RecommendItemsToItem, SearchItems, Batch, ResetDatabase
import random
NUM = 100
PROBABILITY_PURCHASED = 0.1
client = RecombeeClient('--my-database-id--', '--db-private-token--')
client = RecombeeClient('--my-database-id--', '--db-private-token--', region=Region.AP_SE)
# Clear the entire database
client.send(ResetDatabase())
Expand Down
149 changes: 87 additions & 62 deletions recombee_api_client/api_client.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
import os
import time
import hmac
import json
from hashlib import sha1
import requests
import hmac
from typing import Union
from enum import Enum

try:
from urllib import quote
except ImportError:
from urllib.parse import quote
import requests
from hashlib import sha1
from urllib.parse import quote

from recombee_api_client.exceptions import ApiTimeoutException, ResponseException
from recombee_api_client.api_requests import Batch
from recombee_api_client.api_requests import Batch, Request


class Region(Enum):
"""
Region of the Recombee cluster
"""
AP_SE = 1
CA_EAST = 2
EU_WEST = 3
US_WEST = 4


class RecombeeClient:
"""
Expand All @@ -22,22 +32,19 @@ class RecombeeClient:
:param token: Secret token obtained from Recombee for signing requests
:param protocol: Default protocol for sending requests. Possible values: 'http', 'https'.
:param region: region of the Recombee cluster where the database is located
"""
BATCH_MAX_SIZE = 10000

def __init__(self, database_id, token, protocol = 'https', options = {}):
def __init__(self, database_id: str, token: str, protocol: str = 'https', options: dict = None, region: Region = None):
self.database_id = database_id
self.token = token
self.protocol = protocol

self.base_uri = os.environ.get('RAPI_URI')
if self.base_uri is None:
self.base_uri = options.get('base_uri')
if self.base_uri is None:
self.base_uri = 'rapi.recombee.com'

self.base_uri = self.__get_base_uri(options=options or {}, region=region)

def send(self, request):
def send(self, request: Request) -> Union[dict, str, list]:
"""
:param request: Request to be sent to Recombee recommender
"""
Expand All @@ -63,101 +70,119 @@ def send(self, request):
raise ApiTimeoutException(request)

@staticmethod
def __get_http_headers(additional_headers=None):
headers = {'User-Agent': 'recombee-python-api-client/3.2.0'}
def __get_regional_base_uri(region: Region) -> str:
uri = {
Region.AP_SE: 'rapi-ap-se.recombee.com',
Region.CA_EAST: 'rapi-ca-east.recombee.com',
Region.EU_WEST: 'rapi-eu-west.recombee.com',
Region.US_WEST: 'rapi-us-west.recombee.com'
}.get(region)

if uri is None:
raise ValueError('Unknown region given')
return uri

@staticmethod
def __get_base_uri(options: dict, region: str) -> str:
base_uri = os.environ.get('RAPI_URI') or options.get('base_uri')
if region is not None:
if base_uri:
raise ValueError('base_uri and region cannot be specified at the same time')
base_uri = RecombeeClient.__get_regional_base_uri(region)

return base_uri or 'rapi.recombee.com'

@staticmethod
def __get_http_headers(additional_headers: dict = None) -> dict:
headers = {'User-Agent': 'recombee-python-api-client/4.0.0'}
if additional_headers:
headers.update(additional_headers)
return headers

def __put(self, request, uri, timeout):
def __put(self, request: Request, uri: str, timeout: int):
response = requests.put(uri,
data=json.dumps(request.get_body_parameters()),
headers= self.__get_http_headers({'Content-Type': 'application/json'}),
headers=self.__get_http_headers({'Content-Type': 'application/json'}),
timeout=timeout)
self.__check_errors(response, request)
return response.json()

def __get(self, request, uri, timeout):
def __get(self, request: Request, uri: str, timeout: int):
response = requests.get(uri,
headers= self.__get_http_headers(),
headers=self.__get_http_headers(),
timeout=timeout)
self.__check_errors(response, request)
return response.json()

def __post(self, request, uri, timeout):
def __post(self, request: Request, uri: str, timeout: int):
response = requests.post(uri,
data=json.dumps(request.get_body_parameters()),
headers= self.__get_http_headers({'Content-Type': 'application/json'}),
timeout=timeout)
data=json.dumps(request.get_body_parameters()),
headers=self.__get_http_headers({'Content-Type': 'application/json'}),
timeout=timeout)
self.__check_errors(response, request)
return response.json()

def __delete(self, request, uri, timeout):
def __delete(self, request: Request, uri: str, timeout: int):
response = requests.delete(uri,
headers= self.__get_http_headers(),
timeout=timeout)
data=json.dumps(request.get_body_parameters()),
headers=self.__get_http_headers({'Content-Type': 'application/json'}),
timeout=timeout)
self.__check_errors(response, request)
return response.json()


def __check_errors(self, response, request):
def __check_errors(self, response, request: Request):
status_code = response.status_code
if status_code == 200 or status_code == 201:
return
raise ResponseException(request, status_code, response.text)

@staticmethod
def __get_list_chunks(l, n):
def __get_list_chunks(l: list, n: int) -> list:
"""Yield successive n-sized chunks from l."""

try: #Python 2/3 compatibility
xrange
except NameError:
xrange = range

for i in xrange(0, len(l), n):
for i in range(0, len(l), n):
yield l[i:i + n]

def __send_multipart_batch(self, batch):
def __send_multipart_batch(self, batch: Batch) -> list:
requests_parts = [rqs for rqs in self.__get_list_chunks(batch.requests, self.BATCH_MAX_SIZE)]
responses = [self.send(Batch(rqs)) for rqs in requests_parts]
return sum(responses, [])

def __process_request_uri(self, request):
def __process_request_uri(self, request: Request) -> str:
uri = request.path
uri += self.__query_parameters_to_url(request)
return uri

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

def __query_parameters_to_url(self, request):
ps = ''
query_params = request.get_query_parameters()
for name in query_params:
val = query_params[name]
ps += '&' if ps.find('?')!=-1 else '?'
ps += "%s=%s" % (name, self.__format_query_parameter_value(val))
return ps

def __format_query_parameter_value(self, value):
if isinstance(value, list):
return ','.join([quote(str(v)) for v in value])
return quote(str(value))
@staticmethod
def __format_query_parameter_value(value) -> str:
if isinstance(value, list):
return ','.join([quote(str(v)) for v in value])
return quote(str(value))

# Sign request with HMAC, request URI must be exacly the same
# Sign request with HMAC, request URI must be exactly the same
# We have 30s to complete request with this token
def __sign_url(self, req_part):
def __sign_url(self, req_part: str) -> str:
uri = '/' + self.database_id + req_part
time = self.__hmac_time(uri)
sign = self.__hmac_sign(uri, time)
res = uri + time + '&hmac_sign=' +sign
time_part = self.__hmac_time(uri)
sign = self.__hmac_sign(uri, time_part)
res = uri + time_part + '&hmac_sign=' + sign
return res

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

def __hmac_sign(self, uri, time):
url = uri + time
def __hmac_sign(self, uri: str, time_part: str) -> str:
url = uri + time_part
sign = hmac.new(str.encode(self.token), str.encode(url), sha1).hexdigest()
return sign
5 changes: 3 additions & 2 deletions recombee_api_client/api_requests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from recombee_api_client.api_requests.delete_item_property import DeleteItemProperty
from recombee_api_client.api_requests.get_item_property_info import GetItemPropertyInfo
from recombee_api_client.api_requests.list_item_properties import ListItemProperties
from recombee_api_client.api_requests.update_more_items import UpdateMoreItems
from recombee_api_client.api_requests.delete_more_items import DeleteMoreItems
from recombee_api_client.api_requests.add_series import AddSeries
from recombee_api_client.api_requests.delete_series import DeleteSeries
from recombee_api_client.api_requests.list_series import ListSeries
Expand Down Expand Up @@ -58,12 +60,11 @@
from recombee_api_client.api_requests.recommend_next_items import RecommendNextItems
from recombee_api_client.api_requests.recommend_users_to_user import RecommendUsersToUser
from recombee_api_client.api_requests.recommend_users_to_item import RecommendUsersToItem
from recombee_api_client.api_requests.user_based_recommendation import UserBasedRecommendation
from recombee_api_client.api_requests.item_based_recommendation import ItemBasedRecommendation
from recombee_api_client.api_requests.search_items import SearchItems
from recombee_api_client.api_requests.add_search_synonym import AddSearchSynonym
from recombee_api_client.api_requests.list_search_synonyms import ListSearchSynonyms
from recombee_api_client.api_requests.delete_all_search_synonyms import DeleteAllSearchSynonyms
from recombee_api_client.api_requests.delete_search_synonym import DeleteSearchSynonym
from recombee_api_client.api_requests.reset_database import ResetDatabase
from recombee_api_client.api_requests.batch import Batch
from recombee_api_client.api_requests.request import Request
12 changes: 5 additions & 7 deletions recombee_api_client/api_requests/add_bookmark.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from recombee_api_client.api_requests.request import Request
from typing import Union, List
import uuid

DEFAULT = uuid.uuid4()
Expand Down Expand Up @@ -27,19 +28,16 @@ class AddBookmark(Request):
"""

def __init__(self, user_id, item_id, timestamp=DEFAULT, cascade_create=DEFAULT, recomm_id=DEFAULT, additional_data=DEFAULT):
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):
super().__init__(path="/bookmarks/" % (), method='post', timeout=1000, ensure_https=False)
self.user_id = user_id
self.item_id = item_id
self.timestamp = timestamp
self.cascade_create = cascade_create
self.recomm_id = recomm_id
self.additional_data = additional_data
self.timeout = 1000
self.ensure_https = False
self.method = 'post'
self.path = "/bookmarks/" % ()

def get_body_parameters(self):
def get_body_parameters(self) -> dict:
"""
Values of body parameters as a dictionary (name of parameter: value of the parameter).
"""
Expand All @@ -56,7 +54,7 @@ def get_body_parameters(self):
p['additionalData'] = self.additional_data
return p

def get_query_parameters(self):
def get_query_parameters(self) -> dict:
"""
Values of query parameters as a dictionary (name of parameter: value of the parameter).
"""
Expand Down
12 changes: 5 additions & 7 deletions recombee_api_client/api_requests/add_cart_addition.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from recombee_api_client.api_requests.request import Request
from typing import Union, List
import uuid

DEFAULT = uuid.uuid4()
Expand Down Expand Up @@ -31,7 +32,8 @@ class AddCartAddition(Request):
"""

def __init__(self, user_id, item_id, timestamp=DEFAULT, cascade_create=DEFAULT, amount=DEFAULT, price=DEFAULT, recomm_id=DEFAULT, additional_data=DEFAULT):
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):
super().__init__(path="/cartadditions/" % (), method='post', timeout=1000, ensure_https=False)
self.user_id = user_id
self.item_id = item_id
self.timestamp = timestamp
Expand All @@ -40,12 +42,8 @@ def __init__(self, user_id, item_id, timestamp=DEFAULT, cascade_create=DEFAULT,
self.price = price
self.recomm_id = recomm_id
self.additional_data = additional_data
self.timeout = 1000
self.ensure_https = False
self.method = 'post'
self.path = "/cartadditions/" % ()

def get_body_parameters(self):
def get_body_parameters(self) -> dict:
"""
Values of body parameters as a dictionary (name of parameter: value of the parameter).
"""
Expand All @@ -66,7 +64,7 @@ def get_body_parameters(self):
p['additionalData'] = self.additional_data
return p

def get_query_parameters(self):
def get_query_parameters(self) -> dict:
"""
Values of query parameters as a dictionary (name of parameter: value of the parameter).
"""
Expand Down
Loading

0 comments on commit 8f0829d

Please sign in to comment.