Skip to content

Commit c9bf748

Browse files
committed
Ruby API client
0 parents  commit c9bf748

File tree

124 files changed

+4249
-0
lines changed

Some content is hidden

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

124 files changed

+4249
-0
lines changed

Gemfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
source 'https://rubygems.org'
2+
3+
gemspec

LICENSE.txt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2015 Ondřej Fiedler
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in
13+
all copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
THE SOFTWARE.

README.md

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# RecombeeApiClient
2+
3+
A Ruby client for easy use of the [Recombee](https://www.recombee.com/) recommendation API.
4+
5+
Documentation of the API can be found at [docs.recombee.com](https://docs.recombee.com/).
6+
7+
## Installation
8+
9+
Add this line to your application's Gemfile:
10+
11+
```ruby
12+
gem 'recombee_api_client'
13+
```
14+
15+
And then execute:
16+
17+
$ bundle
18+
19+
Or install it yourself as:
20+
21+
$ gem install recombee_api_client
22+
23+
## Examples
24+
25+
### Basic example
26+
```ruby
27+
require 'recombee_api_client'
28+
include RecombeeApiClient
29+
30+
# Prepare some items and users
31+
NUM = 100
32+
my_users = (1..NUM).map { |i| "user-#{i}" }
33+
my_items = (1..NUM).map { |i| "item-#{i}" }
34+
35+
#Generate some random purchases of items by users
36+
PROBABILITY_PURCHASED = 0.1
37+
my_purchases = []
38+
my_users.each do |user|
39+
p = my_items.select { |_| rand(0.0..1.0) < PROBABILITY_PURCHASED }
40+
p.each { |item| my_purchases.push('userId' => user, 'itemId' => item) }
41+
end
42+
43+
# Use Recombee recommender
44+
client = RecombeeClient.new('client-test', 'jGGQ6ZKa8rQ1zTAyxTc0EMn55YPF7FJLUtaMLhbsGxmvwxgTwXYqmUk5xVZFw98L')
45+
begin
46+
# Send the data to Recombee, use Batch for faster processing
47+
puts 'Send users'
48+
client.send(Batch.new(my_users.map { |userId| AddUser.new(userId) }))
49+
puts 'Send items'
50+
client.send(Batch.new(my_items.map { |itemId| AddItem.new(itemId) }))
51+
puts 'Send purchases'
52+
client.send(Batch.new(my_purchases.map { |p| AddPurchase.new(p['userId'], p['itemId'], 0) }))
53+
54+
# Get recommendations for user 'user-25'
55+
puts 'Recommend for a user'
56+
recommended = client.send(UserBasedRecommendation.new('user-25', 5, 'rotationRate' => 0))
57+
puts "Recommended items: #{recommended}"
58+
rescue ResponseError => e
59+
puts e
60+
end
61+
```
62+
63+
### Using property values
64+
```ruby
65+
#!/usr/bin/env ruby
66+
67+
require 'recombee_api_client'
68+
include RecombeeApiClient
69+
70+
NUM = 100
71+
PROBABILITY_PURCHASED = 0.1
72+
73+
client = RecombeeClient.new('client-test', 'jGGQ6ZKa8rQ1zTAyxTc0EMn55YPF7FJLUtaMLhbsGxmvwxgTwXYqmUk5xVZFw98L')
74+
client.send(ResetDatabase.new)
75+
# We will use computers as items in this example
76+
# Computers have three properties
77+
# - price (floating point number)
78+
# - number of processor cores (integer number)
79+
# - description (string)
80+
81+
# Add properties of items
82+
client.send(AddItemProperty.new('price', 'double'))
83+
client.send(AddItemProperty.new('num-cores', 'int'))
84+
client.send(AddItemProperty.new('description', 'string'))
85+
86+
# Prepare requests for setting a catalog of computers
87+
requests = (1..NUM).map do |i|
88+
SetItemValues.new(
89+
"computer-#{i}", #itemId
90+
#values:
91+
{
92+
'price' => rand(15000.0 .. 25000.0),
93+
'num-cores' => rand(1..8),
94+
'description' => 'Great computer',
95+
'!cascadeCreate' => true # Use !cascadeCreate for creating item
96+
# with given itemId, if it doesn't exist
97+
}
98+
)
99+
end
100+
101+
# Send catalog to the recommender system
102+
client.send(Batch.new(requests))
103+
104+
# Prepare some purchases of items by users
105+
requests = []
106+
(1..NUM).map{|i| "computer-#{i}"}.each do |item_id|
107+
user_ids = (1..NUM).map{|i| "user-#{i}"}
108+
user_ids = user_ids.select { |_| rand(0.0..1.0) < PROBABILITY_PURCHASED }
109+
# Use cascadeCreate to create unexisting users
110+
user_ids.each { |user_id| requests.push(AddPurchase.new(user_id, item_id, 0, 'cascadeCreate' => true)) }
111+
end
112+
113+
# Send purchases to the recommender system
114+
client.send(Batch.new(requests))
115+
116+
# Get 5 recommendations for user-42, who is currently viewing computer-6
117+
recommended = client.send(ItemBasedRecommendation.new('computer-6', 5, 'targetUserId' => 'user-42') )
118+
puts "Recommended items: #{recommended}"
119+
120+
# Get 5 recommendations for user-42, but recommend only computers that
121+
# have at least 3 cores
122+
recommended = client.send(
123+
ItemBasedRecommendation.new('computer-6', 5, {'targetUserId' => 'user-42', 'filter' => "'num-cores'>=3"})
124+
)
125+
puts "Recommended items with at least 3 processor cores: #{recommended}"
126+
127+
# Get 5 recommendations for user-42, but recommend only items that
128+
# are more expensive then currently viewed item (up-sell)
129+
recommended = client.send(
130+
ItemBasedRecommendation.new('computer-6', 5,
131+
{'targetUserId' => 'user-42', 'filter' => "'price' > context_item[\"price\"]"})
132+
)
133+
puts "Recommended up-sell items: #{recommended}"
134+
```
135+
136+
### Exception handling
137+
138+
For the sake of brevity, the above examples omit exception handling. However, various exceptions can occur while processing request, for example because of adding an already existing item, submitting interaction of nonexistent user or because of timeout.
139+
140+
We are doing our best to provide the fastest and most reliable service, but production-level applications must implement a fallback solution since errors can always happen. The fallback might be, for example, showing the most popular items from the current category, or not displaying recommendations at all.
141+
142+
Example:
143+
```ruby
144+
145+
begin
146+
recommended = client.send(
147+
ItemBasedRecommendation.new('computer-6', 5,
148+
{'targetUserId' => 'user-42', 'filter' => "'price' > context_item[\"price\"]"})
149+
)
150+
rescue ResponseError => e
151+
#Handle errorneous request => use fallback
152+
rescue ApiTimeout => e
153+
#Handle timeout => use fallback
154+
rescue APIError => e
155+
#APIError is parent of both ResponseError and ApiTimeout
156+
end
157+
```

Rakefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
require 'bundler/gem_tasks'
2+
require 'rspec/core/rake_task'
3+
4+
RSpec::Core::RakeTask.new(:spec)
5+
6+
task default: :spec

lib/recombee_api_client.rb

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
require 'recombee_api_client/version'
2+
require 'securerandom'
3+
require 'digest/hmac'
4+
require 'httparty'
5+
require 'json'
6+
require 'open-uri'
7+
require 'net/https'
8+
require 'timeout'
9+
10+
require 'recombee_api_client/errors'
11+
Gem.find_files('recombee_api_client/api/*.rb').each { |path| require path }
12+
13+
module RecombeeApiClient
14+
class RecombeeClient
15+
include HTTParty
16+
17+
def initialize(account, token, options = {})
18+
@account = account
19+
@token = token
20+
@base_uri = options[:base_uri] ||= 'https://rapi.recombee.com'
21+
end
22+
23+
def send(request)
24+
@request = request
25+
uri = request.path
26+
uri.slice! ('/{databaseId}/')
27+
uri = URI.escape uri
28+
timeout = request.timeout / 1000
29+
# puts uri
30+
begin
31+
case request.method
32+
when :put
33+
hmac_put(uri, timeout)
34+
when :get
35+
hmac_get(uri, timeout)
36+
when :post
37+
hmac_post(uri, timeout, request.body_parameters.to_json)
38+
when :delete
39+
hmac_delete(uri, timeout)
40+
end
41+
rescue Timeout::Error
42+
fail ApiTimeout.new(@request)
43+
end
44+
end
45+
46+
private
47+
48+
def hmac_put(uri, timeout, options = {})
49+
r = self.class.put(sign_url(uri), query: options, timeout: timeout)
50+
check_errors r
51+
r.body
52+
end
53+
54+
def hmac_get(uri, timeout, options = {})
55+
r = self.class.get(sign_url(uri), query: options, timeout: timeout)
56+
check_errors r
57+
JSON.parse(r.body)
58+
end
59+
60+
def hmac_post(uri, timeout, options = {})
61+
url = sign_url(uri)
62+
# pass arguments in body
63+
r = self.class.post(url, body: options,
64+
headers: { 'Content-Type' => 'application/json' },
65+
timeout: timeout)
66+
check_errors r
67+
begin
68+
return JSON.parse(r.body)
69+
rescue JSON::ParserError
70+
return r.body
71+
end
72+
end
73+
74+
def hmac_delete(uri, timeout, options = {})
75+
r = self.class.delete(sign_url(uri), query: options, timeout: timeout)
76+
check_errors r
77+
r.body
78+
end
79+
80+
def check_errors(response)
81+
status_code = response.code
82+
return if status_code == 200 || status_code == 201
83+
fail ResponseError.new(@request, status_code, response.body)
84+
end
85+
86+
# Sign request with HMAC, request URI must be exacly the same
87+
# We have 30s to complete request with this token
88+
def sign_url(req)
89+
uri = "/#{@account}/#{req}"
90+
time = hmac_time(uri)
91+
sign = hmac_sign(uri, time)
92+
@base_uri + uri + time + "&hmac_sign=#{sign}"
93+
end
94+
95+
def hmac_time(uri)
96+
res = (uri.include? '?') ? '&' : '?'
97+
res << "hmac_timestamp=#{Time.now.utc.to_i}"
98+
end
99+
100+
def hmac_sign(uri, time)
101+
url = uri + time
102+
Digest::HMAC.hexdigest(url, @token, Digest::SHA1)
103+
end
104+
end
105+
end
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#
2+
# This file is auto-generated, do not edit
3+
#
4+
5+
module RecombeeApiClient
6+
require_relative 'request'
7+
require_relative '../errors'
8+
9+
##
10+
#Adds a bookmark of a given item made by a given user.
11+
#
12+
class AddBookmark < ApiRequest
13+
attr_reader :user_id, :item_id, :timestamp, :cascade_create
14+
attr_accessor :timeout
15+
16+
##
17+
# * *Required arguments*
18+
# - +user_id+ -> User who bookmarked the item
19+
# - +item_id+ -> Bookmarked item
20+
# - +timestamp+ -> Unix timestamp of the bookmark. If you don't have the timestamp value available, you may use some artificial value, such as 0. It is preferable, however, to provide the timestamp whenever possible as the user's preferences may evolve over time.
21+
#
22+
# * *Optional arguments (given as hash optional)*
23+
# - +cascadeCreate+ -> Sets whether the given user/item should be created if not present in the database.
24+
#
25+
def initialize(user_id, item_id, timestamp, optional = {})
26+
@user_id = user_id
27+
@item_id = item_id
28+
@timestamp = timestamp
29+
@cascade_create = optional['cascadeCreate']
30+
@optional = optional
31+
@timeout = 1000
32+
@optional.each do |par, _|
33+
fail UnknownOptionalParameter.new(par) unless ["cascadeCreate"].include? par
34+
end
35+
end
36+
37+
# HTTP method
38+
def method
39+
:post
40+
end
41+
42+
# Values of body parameters as a Hash
43+
def body_parameters
44+
p = Hash.new
45+
p['userId'] = @user_id
46+
p['itemId'] = @item_id
47+
p['timestamp'] = @timestamp
48+
p['cascadeCreate'] = @optional['cascadeCreate'] if @optional['cascadeCreate']
49+
p
50+
end
51+
52+
# Values of query path parameters as a Hash.
53+
# name of parameter => value of the parameter
54+
def query_parameters
55+
params = {}
56+
params
57+
end
58+
59+
# Relative path to the endpoint
60+
def basic_path
61+
"/{databaseId}/bookmarks/"
62+
end
63+
64+
# Relative path to the endpoint including query parameters
65+
def path
66+
p = "/{databaseId}/bookmarks/"
67+
p
68+
end
69+
end
70+
end

0 commit comments

Comments
 (0)