aliexpress 0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 72fe66faf65e1f93edd912f2ba24148ca70c5ae1
4
+ data.tar.gz: 5ec3aa35f98e2deea15312fd5d243169d281dd43
5
+ SHA512:
6
+ metadata.gz: db423a70fbb44d9063a2e7e5f809aaa3948a0bf7bbf7bcd5ea69bb7b53e53d75e3e77de01cf51d509eafcd193182d3f635b754b060fdc7b902f5f49b96344cec
7
+ data.tar.gz: 2caf268645d24586782369475cb5437e7ced287508746db1e4afa52616df55130638dd558bfb51b5f4ae1101f6046ca26e2b9ef5ec4a9fd524e14a0e6255c8a2
@@ -0,0 +1 @@
1
+ service_name: travis-ci
@@ -0,0 +1,11 @@
1
+ *.gem
2
+ /Gemfile.lock
3
+ /.approvals
4
+ /.bundle
5
+ /coverage
6
+ .DS_Store
7
+
8
+ /.client_id
9
+ /.client_secret
10
+ /.access_token
11
+ /.refresh_token
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
@@ -0,0 +1,38 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.2
3
+
4
+ Metrics/AbcSize:
5
+ Enabled: false
6
+ Metrics/CyclomaticComplexity:
7
+ Enabled: false
8
+ Metrics/PerceivedComplexity:
9
+ Enabled: false
10
+
11
+ Metrics/LineLength:
12
+ Enabled: false
13
+ Metrics/ClassLength:
14
+ Enabled: false
15
+ Metrics/MethodLength:
16
+ Enabled: false
17
+ Metrics/BlockLength:
18
+ Enabled: false
19
+ Metrics/ParameterLists:
20
+ Enabled: false
21
+
22
+ Lint/EndAlignment:
23
+ Enabled: false
24
+
25
+ Style/Documentation:
26
+ Enabled: false
27
+ Style/IndentationWidth:
28
+ Enabled: false
29
+ Style/ElseAlignment:
30
+ Enabled: false
31
+ Style/GuardClause:
32
+ Enabled: false
33
+ Style/NumericLiterals:
34
+ Enabled: false
35
+
36
+ # This is temporary - it can be remove after updating rubocop to the latest.
37
+ Style/PercentLiteralDelimiters:
38
+ Enabled: false
@@ -0,0 +1,10 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2
4
+ # branches:
5
+ # only:
6
+ # - master
7
+ script:
8
+ - bundle exec rubocop
9
+ - bundle exec rake
10
+ cache: bundler
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ group :development, :test do
5
+ gem 'awesome_print'
6
+ gem 'pry'
7
+ gem 'rake'
8
+ gem 'rubocop'
9
+ end
10
+
11
+ group :test do
12
+ gem 'coveralls', require: false
13
+ gem 'rspec'
14
+ gem 'webmock'
15
+ end
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Tophatter
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 all
13
+ 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 THE
21
+ SOFTWARE.
@@ -0,0 +1,117 @@
1
+ [![Build Status](https://travis-ci.org/tophatter/aliexpress-api-ruby.svg?branch=master)](https://travis-ci.org/tophatter/aliexpress-api-ruby)
2
+ [![Coverage Status](https://coveralls.io/repos/github/tophatter/aliexpress-api-ruby/badge.svg?branch=master)](https://coveralls.io/github/tophatter/aliexpress-api-ruby?branch=master)
3
+
4
+ ### AliExpress SDK for Ruby:
5
+
6
+ #### Authentication:
7
+
8
+ To use this SDK, you must have an app key and app secret (a.k.a. client id and client secret). These can be obtained [here](https://portals.aliexpress.com/adcenter/apiSetting.htm) if you're using the affiliate program, or [here](http://isvus.aliexpress.com/isv/index.htm) if you're using the ISV API.
9
+
10
+ In the affiliate program API settings, you'll see the following:
11
+
12
+ ![](https://i.imgur.com/ycMNgMY.png)
13
+
14
+ The "api key" is your client id, and the "digital signature" is your client secret.
15
+
16
+ The API uses OAuth for authentication. You need to first go through a manual authorization sequence to get a short-live authorization code. That code can be exchanged for an access token that last 10 hours, and a refresh token that lasts significantly longer.
17
+
18
+ Using this gem's console, you get obtain an access token as follows:
19
+
20
+ ```
21
+ bash $ bin/console
22
+ [1] pry(main)> AliExpress.client_id = [YOUR CLIENT ID]
23
+ [2] pry(main)> AliExpress.client_secret = [YOUR CLIENT SECRET]
24
+ [3] pry(main)> AliExpress::Auth.authorize
25
+ Open https://gw.api.alibaba.com/auth/authorize.htm?_aop_signature=REDACTED&client_id=REDACTED&redirect_uri=REDACTED&site=aliexpress&state=REDACTED in your browser, and then enter the code below.
26
+ authorization_code> [ENTER YOUR CODE HERE]
27
+ {
28
+ "refresh_token_timeout" => "20180402143144000-0700",
29
+ "aliId" => "[REDACTED]",
30
+ "resource_owner" => "[REDACTED]",
31
+ :access_token => "[REDACTED]",
32
+ :refresh_token => "[REDACTED]",
33
+ :expires_at => 1507194954
34
+ }
35
+ ```
36
+
37
+ Once your tokens are obtained, you can instruct this SDK to use them:
38
+
39
+ ```
40
+ AliExpress.access_token = [YOUR ACCESS TOKEN]
41
+ AliExpress.refresh_token = [YOUR REFRESH TOKEN]
42
+ ```
43
+
44
+ If your access token expires, you can get a new one with the following:
45
+
46
+ ```
47
+ AliExpress::Auth.refresh
48
+ ```
49
+
50
+ #### Affiliate API:
51
+
52
+ ##### api.listPromotionProduct
53
+
54
+ ```
55
+ [1] pry(main)> AliExpress::Affiliate.list_promotion_product(keywords: 'drone')
56
+ D, [2017-10-05T11:23:03.971387 #69079] DEBUG -- : GET https://gw.api.alibaba.com/openapi/param2/2/portals.open/api.listPromotionProduct/[REDACTED]?fields=productId%2CproductTitle%2CproductUrl%2CimageUrl%2CallImageUrls%2CoriginalPrice%2CsalePrice%2ClocalPrice%2Cdiscount%2Cvolume%2CpackageType%2ClotNum%2CvalidTime&keywords=drone
57
+ [
58
+ [ 0] {
59
+ "lotNum" => 1,
60
+ "packageType" => "piece",
61
+ "imageUrl" => "https://ae01.alicdn.com/kf/HTB1nxEjSFXXXXcDXpXXq6xXFXXXF/100-Original-SYMA-X5C-Upgrade-Version-RC-font-b-Drone-b-font-With-2MP-HD-Camera.jpg",
62
+ "allImageUrls" => "https://ae01.alicdn.com/kf/HTB1nxEjSFXXXXcDXpXXq6xXFXXXF/100-Original-SYMA-X5C-Upgrade-Version-RC-font-b-Drone-b-font-With-2MP-HD-Camera.jpg,https://ae01.alicdn.com/kf/HTB1O6LUSFXXXXX5aXXXq6xXFXXXB/100-Original-SYMA-X5C-Upgrade-Version-RC-font-b-Drone-b-font-With-2MP-HD-Camera.jpg,https://ae01.alicdn.com/kf/HTB1aA.DSFXXXXXgXXXXq6xXFXXX3/100-Original-SYMA-X5C-Upgrade-Version-RC-font-b-Drone-b-font-With-2MP-HD-Camera.jpg,https://ae01.alicdn.com/kf/HTB1N.kySFXXXXaUXXXXq6xXFXXXF/100-Original-SYMA-X5C-Upgrade-Version-RC-font-b-Drone-b-font-With-2MP-HD-Camera.jpg,https://ae01.alicdn.com/kf/HTB1lc_1SFXXXXcgXVXXq6xXFXXX9/100-Original-SYMA-X5C-Upgrade-Version-RC-font-b-Drone-b-font-With-2MP-HD-Camera.jpg,https://ae01.alicdn.com/kf/HTB16lEkSFXXXXcpXpXXq6xXFXXXK/100-Original-SYMA-X5C-Upgrade-Version-RC-font-b-Drone-b-font-With-2MP-HD-Camera.jpg",
63
+ "productId" => 32817202058,
64
+ "discount" => "5%",
65
+ "originalPrice" => "US $27.99",
66
+ "productTitle" => "100% Original SYMA X5C (Upgrade Version) RC <font><b>Drone</b></font> With 2MP HD Camera 6-Axis RC Quadcopter Helicopter X5 Dron Without Camera",
67
+ "validTime" => "2017-10-08",
68
+ "productUrl" => "https://www.aliexpress.com/item/SYMA-X5C-1-Upgrade-Version-SYMA-X5C-Professional-RC-Drone-6-Axis-Helicopter-Quadcopter-With-2MP/32817202058.html",
69
+ "volume" => "14",
70
+ "salePrice" => "US $26.59"
71
+ },
72
+ <snip>
73
+ ```
74
+
75
+ ##### api.getPromotionProductDetail
76
+
77
+ ```
78
+ [1] pry(main)> AliExpress::Affiliate.get_promotion_product_detail(32817202058)
79
+ D, [2017-10-05T11:24:19.235970 #69079] DEBUG -- : GET https://gw.api.alibaba.com/openapi/param2/2/portals.open/api.getPromotionProductDetail/[REDACTED]?fields=productId%2CproductTitle%2CproductUrl%2CimageUrl%2CallImageUrls%2CoriginalPrice%2CsalePrice%2ClocalPrice%2Cdiscount%2Cvolume%2CpackageType%2ClotNum%2CvalidTime&productId=32817202058
80
+ {
81
+ "lotNum" => 1,
82
+ "packageType" => "piece",
83
+ "imageUrl" => "https://ae01.alicdn.com/kf/HTB1nxEjSFXXXXcDXpXXq6xXFXXXF/100-Original-SYMA-X5C-Upgrade-Version-RC-Drone-With-2MP-HD-Camera-6-Axis-RC-Quadcopter.jpg",
84
+ "allImageUrls" => "https://ae01.alicdn.com/kf/HTB1nxEjSFXXXXcDXpXXq6xXFXXXF/100-Original-SYMA-X5C-Upgrade-Version-RC-Drone-With-2MP-HD-Camera-6-Axis-RC-Quadcopter.jpg,https://ae01.alicdn.com/kf/HTB1O6LUSFXXXXX5aXXXq6xXFXXXB/100-Original-SYMA-X5C-Upgrade-Version-RC-Drone-With-2MP-HD-Camera-6-Axis-RC-Quadcopter.jpg,https://ae01.alicdn.com/kf/HTB1aA.DSFXXXXXgXXXXq6xXFXXX3/100-Original-SYMA-X5C-Upgrade-Version-RC-Drone-With-2MP-HD-Camera-6-Axis-RC-Quadcopter.jpg,https://ae01.alicdn.com/kf/HTB1N.kySFXXXXaUXXXXq6xXFXXXF/100-Original-SYMA-X5C-Upgrade-Version-RC-Drone-With-2MP-HD-Camera-6-Axis-RC-Quadcopter.jpg,https://ae01.alicdn.com/kf/HTB1lc_1SFXXXXcgXVXXq6xXFXXX9/100-Original-SYMA-X5C-Upgrade-Version-RC-Drone-With-2MP-HD-Camera-6-Axis-RC-Quadcopter.jpg,https://ae01.alicdn.com/kf/HTB16lEkSFXXXXcpXpXXq6xXFXXXK/100-Original-SYMA-X5C-Upgrade-Version-RC-Drone-With-2MP-HD-Camera-6-Axis-RC-Quadcopter.jpg",
85
+ "productId" => 32817202058,
86
+ "discount" => "5%",
87
+ "originalPrice" => "US $27.99",
88
+ "productTitle" => "100% Original SYMA X5C (Upgrade Version) RC Drone With 2MP HD Camera 6-Axis RC Quadcopter Helicopter X5 Dron Without Camera",
89
+ "validTime" => "2017-10-08",
90
+ "productUrl" => "https://www.aliexpress.com/item/SYMA-X5C-1-Upgrade-Version-SYMA-X5C-Professional-RC-Drone-6-Axis-Helicopter-Quadcopter-With-2MP/32817202058.html",
91
+ "volume" => "14",
92
+ "salePrice" => "US $26.59"
93
+ }
94
+ ```
95
+
96
+ ##### api.listHotProducts
97
+
98
+ ```
99
+ [1] pry(main)> AliExpress::Affiliate.list_hot_products(3)
100
+ D, [2017-10-05T11:21:54.835675 #68982] DEBUG -- : GET https://gw.api.alibaba.com/openapi/param2/2/portals.open/api.listHotProducts/[REDACTED]?categoryId=3&language=en&localCurrency=USD
101
+ [
102
+ [ 0] {
103
+ "localPrice" => "21.00 $",
104
+ "imageUrl" => "https://ae01.alicdn.com/kf/HTB1YsQwQFXXXXcvXpXXq6xXFXXX0/Preself-Hoodies-Sweatshirt-Women-Casual-Outwear-Hoody-Loose-Long-Sleeve-Mantle-Hooded-Cover-Pullover-Clothes-2017.jpg",
105
+ "productUrl" => "https://www.aliexpress.com/item/New-Women-Loose-Fashion-Autumn-Winter-Hooded-Oversize-Sweatshirt-Coat-Outwear/32763830156.html",
106
+ "validTime" => "2017-10-18",
107
+ "productTitle" => "Preself Hoodies Sweatshirt Women Casual Outwear Hoody Loose Long Sleeve Mantle Hooded Cover Pullover Clothes 2017 New",
108
+ "volume" => "",
109
+ "salePrice" => "US $21.00",
110
+ "productId" => 32763830156
111
+ },
112
+ <snip>
113
+ ```
114
+
115
+ #### ISV API:
116
+
117
+ In progress.
@@ -0,0 +1,3 @@
1
+ require 'rspec/core/rake_task'
2
+ RSpec::Core::RakeTask.new(:spec)
3
+ task default: :spec
@@ -0,0 +1,29 @@
1
+ # To publish the next version:
2
+ # gem build aliexpress.gemspec
3
+ # gem push aliexpress-{VERSION}.gem
4
+ Gem::Specification.new do |s|
5
+ s.name = 'aliexpress'
6
+ s.version = '0.4'
7
+ s.platform = Gem::Platform::RUBY
8
+ s.licenses = ['MIT']
9
+ s.authors = ['Chris Estreich']
10
+ s.email = ['chris@tophatter.com']
11
+ s.homepage = 'https://github.com/tophatter/aliexpress-api-ruby'
12
+ s.summary = 'AliExpress SDK for ruby.'
13
+ s.description = 'AliExpress SDK for ruby.'
14
+
15
+ s.extra_rdoc_files = ['README.md']
16
+
17
+ s.required_ruby_version = '~> 2.0'
18
+
19
+ s.add_dependency 'rest-client', '~> 1.6'
20
+ s.add_dependency 'addressable'
21
+ s.add_dependency 'oauth2'
22
+
23
+ s.post_install_message = 'Documentation is available at: http://isvus.aliexpress.com/isv/index.htm'
24
+
25
+ s.files = `git ls-files`.split("\n")
26
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
27
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
28
+ s.require_paths = ['lib']
29
+ end
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup' # Otherwise "require 'aliexpress'" won't work.
4
+ require 'aliexpress'
5
+ require 'awesome_print'
6
+ require 'pry'
7
+
8
+ %w(client_id client_secret access_token refresh_token).each do |method|
9
+ begin
10
+ AliExpress.send("#{method}=", File.read(".#{method}").chomp)
11
+ puts "AliExpress.#{method}: #{AliExpress.send(method)}"
12
+ rescue Errno::ENOENT
13
+ puts "No .#{method} file, using default: #{AliExpress.send(method)}"
14
+ end
15
+ end
16
+
17
+ AliExpress.logger = Logger.new(STDOUT)
18
+ AliExpress.logger.level = Logger::DEBUG
19
+
20
+ AwesomePrint.pry!
21
+ Pry.start
@@ -0,0 +1,45 @@
1
+ require 'json'
2
+ require 'rest-client'
3
+ require 'addressable'
4
+ require 'oauth2'
5
+
6
+ module AliExpress
7
+ class << self
8
+ attr_accessor :client_id, :client_secret, :access_token, :refresh_token
9
+
10
+ attr_writer :protocol, :host, :base_uri
11
+ attr_writer :currency, :language
12
+ attr_writer :logger
13
+
14
+ def protocol
15
+ @protocol || 'https'
16
+ end
17
+
18
+ def host
19
+ @host || 'gw.api.alibaba.com'
20
+ end
21
+
22
+ def base_uri
23
+ @base_uri || "#{protocol}://#{host}/openapi"
24
+ end
25
+
26
+ def currency
27
+ @currency || 'USD'
28
+ end
29
+
30
+ def language
31
+ @language || 'en'
32
+ end
33
+
34
+ def logger
35
+ @logger ||= Logger.new('/dev/null')
36
+ @logger
37
+ end
38
+ end
39
+ end
40
+
41
+ require File.dirname(__FILE__) + '/aliexpress/base'
42
+ require File.dirname(__FILE__) + '/aliexpress/response_error'
43
+ require File.dirname(__FILE__) + '/aliexpress/auth'
44
+ require File.dirname(__FILE__) + '/aliexpress/product'
45
+ require File.dirname(__FILE__) + '/aliexpress/affiliate'
@@ -0,0 +1,141 @@
1
+ module AliExpress
2
+ class Affiliate < Base
3
+ CATEGORIES = {
4
+ 'Apparel & Accessories' => 3,
5
+ 'Automobiles & Motorcycles' => 34,
6
+ 'Beauty & Health' => 66,
7
+ 'Books for Local Russian' => 200004360,
8
+ 'Computer & Office' => 7,
9
+ 'Home Improvement' => 13,
10
+ 'Consumer Electronics' => 44,
11
+ 'Electrical Equipment & Supplies' => 5,
12
+ 'Electronic Components & Supplies' => 502,
13
+ 'Food' => 2,
14
+ 'Furniture' => 1503,
15
+ 'Hair & Accessories' => 200003655,
16
+ 'Hardware' => 42,
17
+ 'Home & Garden' => 15,
18
+ 'Home Appliances' => 6,
19
+ 'Industry & Business' => 200001996,
20
+ 'Jewelry & Accessories' => 36,
21
+ 'Lights & Lighting' => 39,
22
+ 'Luggage & Bags' => 1524,
23
+ 'Mother & Kids' => 1501,
24
+ 'Office & School Supplies' => 21,
25
+ 'Phones & Telecommunications' => 509,
26
+ 'Security & Protection' => 30,
27
+ 'Shoes' => 322,
28
+ 'Special Category' => 200001075,
29
+ 'Sports & Entertainment' => 18,
30
+ 'Tools' => 1420,
31
+ 'Toys & Hobbies' => 26,
32
+ 'Travel and Coupon Services' => 200003498,
33
+ 'Watches' => 1511,
34
+ 'Weddings & Events' => 320
35
+ }.freeze
36
+
37
+ FILTERS = %w(category keywords min_price max_price high_quality).freeze
38
+ SORTS = %w(orignalPriceUp orignalPriceDown sellerRateUp sellerRateDown).freeze
39
+ FIELDS = %w(productId productTitle productUrl imageUrl allImageUrls originalPrice salePrice localPrice discount volume).freeze
40
+
41
+ SUCCESS = 20010000
42
+ ERRORS = {
43
+ 20020000 => 'System error.',
44
+ 20030000 => 'Unauthorized transfer request.',
45
+ 20030010 => 'Required parameters.',
46
+ 20030020 => 'Invalid protocol format.',
47
+ 20030030 => 'API version input parameter error.',
48
+ 20030040 => 'API namespace input parameter error.',
49
+ 20030050 => 'API name input parameter error.',
50
+ 20030060 => 'Fields input parameter error.',
51
+ 20030070 => 'Keyword input parameter error.',
52
+ 20030080 => 'Category ID input parameter error.',
53
+ 20030140 => 'Page number input parameter error.',
54
+ 20030150 => 'Page size input parameter error (default = 20, max = 40).',
55
+ 20030160 => 'Sort input parameter error.'
56
+ }.freeze
57
+
58
+ class << self
59
+ def search(query:, filters: {}, sort: 'orignalPriceUp', page: nil, per_page: 40, fields: FIELDS)
60
+ response = get(
61
+ api_call: 'api.listPromotionProduct',
62
+ params: {
63
+ keywords: query,
64
+ # filters
65
+ categoryId: CATEGORIES[filters[:category]] || filters[:category],
66
+ originalPriceFrom: filters[:min_price],
67
+ originalPriceTo: filters[:max_price],
68
+ highQualityItems: filters[:high_quality],
69
+ # sorting
70
+ sort: sort,
71
+ # paging
72
+ pageNo: page,
73
+ pageSize: per_page,
74
+ # response format
75
+ fields: fields.join(',')
76
+ }.delete_if { |_k, v| v.nil? }
77
+ )
78
+
79
+ raise_response_error(response) unless response['errorCode'] == SUCCESS
80
+ response
81
+ end
82
+
83
+ def find(id, fields: FIELDS)
84
+ response = get(
85
+ api_call: 'api.getPromotionProductDetail',
86
+ params: { productId: id, fields: fields.join(',') }
87
+ )
88
+
89
+ raise_response_error(response) unless response['errorCode'] == SUCCESS
90
+ response
91
+ end
92
+
93
+ def similar(id)
94
+ response = get(
95
+ api_call: 'api.listSimilarProducts',
96
+ params: { productId: id }
97
+ )
98
+
99
+ raise_response_error(response) unless response['errorCode'] == SUCCESS
100
+ response
101
+ end
102
+
103
+ def popular(category)
104
+ response = get(
105
+ api_call: 'api.listHotProducts',
106
+ params: { categoryId: CATEGORIES[category] || category }
107
+ )
108
+
109
+ raise_response_error(response) unless response['errorCode'] == SUCCESS
110
+ response
111
+ end
112
+
113
+ private
114
+
115
+ def get(hash)
116
+ defaults = {
117
+ api_namespace: 'portals.open',
118
+ api_version: 2,
119
+ auth: false,
120
+ sign: false,
121
+ params: {
122
+ localCurrency: currency,
123
+ language: language
124
+ }
125
+ }
126
+ params = defaults[:params].merge(hash.delete(:params))
127
+ super(defaults.merge(hash).merge(params: params))
128
+ end
129
+
130
+ def raise_response_error(response)
131
+ exception = ResponseError.new(
132
+ code: response['error_code'] || response['errorCode'],
133
+ message: response['error_message'] || ERRORS[response['errorCode']] || response['errorCode'],
134
+ response: response
135
+ )
136
+
137
+ raise exception
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,86 @@
1
+ module AliExpress
2
+ class Auth < Base
3
+ class << self
4
+ def authorize(state = nil)
5
+ oauth_client = client
6
+ oauth_params = params(state || SecureRandom.hex(24))
7
+ oauth_params[:_aop_signature] = sign(oauth_params)
8
+ logger.debug oauth_params
9
+ url = oauth_client.authorize_url(oauth_params)
10
+
11
+ if RUBY_PLATFORM == 'x86_64-darwin16'
12
+ begin
13
+ system('open', url)
14
+ rescue Exception => e # rubocop:disable Lint/RescueException
15
+ logger.debug "Call to open failed: #{e.message}"
16
+ end
17
+ end
18
+
19
+ $stdout.puts "Open #{url} in your browser, and then enter the code below."
20
+ $stdout.print 'authorization_code> '
21
+ authorization_code = $stdin.gets
22
+
23
+ token = oauth_client.get_token(
24
+ code: authorization_code,
25
+ grant_type: 'authorization_code',
26
+ need_refresh_token: 'true',
27
+ parse: :json,
28
+ redirect_uri: 'urn:ietf:wg:oauth:2.0:oob'
29
+ ).to_hash
30
+
31
+ token
32
+ end
33
+
34
+ def refresh
35
+ if refresh_token
36
+ response = post(
37
+ api_call: 'getToken',
38
+ api_namespace: 'system.oauth2',
39
+ api_version: '1',
40
+ # protocol: 'http',
41
+ auth: false,
42
+ sign: false,
43
+ params: {
44
+ grant_type: 'refresh_token',
45
+ client_id: client_id,
46
+ client_secret: client_secret,
47
+ refresh_token: refresh_token
48
+ }
49
+ )
50
+ AliExpress.access_token = response['access_token']
51
+ else
52
+ raise Exception, 'You must have a refresh_token to refresh authorization. You need to call `AliExpress::Auth.authorize` and follow the instructions to generate one.'
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def client
59
+ OAuth2::Client.new(
60
+ client_id,
61
+ client_secret,
62
+ site: 'https://gw.api.alibaba.com',
63
+ authorize_url: '/auth/authorize.htm',
64
+ token_url: "/openapi/http/1/system.oauth2/getToken/#{client_id}"
65
+ )
66
+ end
67
+
68
+ def sign(hash)
69
+ OpenSSL::HMAC.hexdigest(
70
+ OpenSSL::Digest.new('sha1'),
71
+ client_secret,
72
+ hash.sort.flatten.join.to_s
73
+ ).upcase
74
+ end
75
+
76
+ def params(state)
77
+ {
78
+ client_id: client_id,
79
+ redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
80
+ site: 'aliexpress',
81
+ state: state
82
+ }
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,87 @@
1
+ module AliExpress
2
+ class Base
3
+ class << self
4
+ def client_id
5
+ AliExpress.client_id
6
+ end
7
+
8
+ def client_secret
9
+ AliExpress.client_secret
10
+ end
11
+
12
+ def access_token
13
+ AliExpress.access_token
14
+ end
15
+
16
+ def refresh_token
17
+ AliExpress.refresh_token
18
+ end
19
+
20
+ def currency
21
+ AliExpress.currency
22
+ end
23
+
24
+ def language
25
+ AliExpress.language
26
+ end
27
+
28
+ def logger
29
+ AliExpress.logger
30
+ end
31
+
32
+ def get(params)
33
+ request(params.merge(method: :get))
34
+ end
35
+
36
+ def post(params)
37
+ request(params.merge(method: :post))
38
+ end
39
+
40
+ private
41
+
42
+ def sign_payload(path, payload)
43
+ signature_factor = path + payload.map { |key, value| "#{key}#{value}" }.sort.join
44
+ payload[:_aop_signature] = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), client_secret, signature_factor).upcase
45
+ logger.debug "signature_factor=#{signature_factor}, signature=#{payload[:_aop_signature]}"
46
+ end
47
+
48
+ def request(method:, protocol: 'param2', api_version: 1, api_namespace: 'aliexpress.open', api_call:, auth: true, sign: true, params: {})
49
+ path = [protocol, api_version, api_namespace, api_call, client_id].join('/')
50
+
51
+ payload = params
52
+ payload[:access_token] = access_token if auth
53
+ sign_payload(path, payload) if sign
54
+ url = [AliExpress.base_uri, path].join('/')
55
+
56
+ resp = if method == :get
57
+ unless params.length.zero?
58
+ uri = Addressable::URI.new
59
+ uri.query_values = params
60
+ url += "?#{uri.query}"
61
+ end
62
+
63
+ logger.debug "GET #{url}"
64
+ RestClient.get(url)
65
+ else
66
+ logger.debug "POST #{url} | #{payload}"
67
+ RestClient.post(url, payload)
68
+ end
69
+
70
+ begin
71
+ JSON.parse(resp.body)
72
+ rescue JSON::ParserError
73
+ resp.body
74
+ end
75
+ rescue RestClient::Exception => exception
76
+ resp = begin
77
+ JSON.parse(exception.response)
78
+ rescue JSON::ParserError
79
+ exception.response
80
+ end
81
+
82
+ logger.debug "#{exception.class.name} (#{exception.response.code}): #{resp.inspect}"
83
+ resp
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,22 @@
1
+ module AliExpress
2
+ class Product < Base
3
+ class << self
4
+ def find(id)
5
+ post(api_call: 'api.findAeProductById', params: { productId: id })
6
+ end
7
+
8
+ private
9
+
10
+ def get(params)
11
+ super(default_params.merge(params))
12
+ end
13
+
14
+ def default_params
15
+ {
16
+ api_namespace: 'aliexpress.open',
17
+ api_version: 1
18
+ }
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ module AliExpress
2
+ class ResponseError < StandardError
3
+ attr_reader :code, :message, :response
4
+
5
+ def initialize(code:, message:, response:)
6
+ @code = code
7
+ @message = message
8
+ @response = response
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,70 @@
1
+ require 'spec_helper'
2
+
3
+ # rspec spec/affiliate_spec.rb
4
+ describe AliExpress::Affiliate do
5
+ let(:response) do
6
+ mock = double
7
+ allow(mock).to receive(:code).and_return(200)
8
+ allow(mock).to receive(:body).and_return(json)
9
+ mock
10
+ end
11
+
12
+ let(:success) { 20010000 }
13
+ let(:hash) { { 'errorCode' => success } }
14
+ let(:json) { hash.to_json }
15
+
16
+ # before do
17
+ # AliExpress.logger = Logger.new(STDOUT)
18
+ # AliExpress.logger.level = Logger::DEBUG
19
+ # end
20
+
21
+ describe '.search' do
22
+ let(:url) { 'https://gw.api.alibaba.com/openapi/param2/2/portals.open/api.listPromotionProduct/fake-client-id?categoryId=3&fields=productId&keywords=drone&language=en&localCurrency=USD&pageNo=2&pageSize=5&sort=orignalPriceDown' }
23
+
24
+ it 'returns a successful response' do
25
+ expect(RestClient).to receive(:get).exactly(1).times.with(url).and_return(response)
26
+ response = AliExpress::Affiliate.search(query: 'drone', filters: { category: 'Apparel & Accessories' }, sort: 'orignalPriceDown', page: 2, per_page: 5, fields: %w(productId))
27
+ expect(response['errorCode']).to eq(success)
28
+ end
29
+ end
30
+
31
+ describe '.find' do
32
+ let(:url) { 'https://gw.api.alibaba.com/openapi/param2/2/portals.open/api.getPromotionProductDetail/fake-client-id?fields=productId&language=en&localCurrency=USD&productId=12345' }
33
+
34
+ it 'returns a product' do
35
+ expect(RestClient).to receive(:get).exactly(1).times.with(url).and_return(response)
36
+ response = AliExpress::Affiliate.find(12345, fields: %w(productId))
37
+ expect(response['errorCode']).to eq(success)
38
+ end
39
+ end
40
+
41
+ describe '.similar' do
42
+ let(:url) { 'https://gw.api.alibaba.com/openapi/param2/2/portals.open/api.listSimilarProducts/fake-client-id?language=en&localCurrency=USD&productId=12345' }
43
+
44
+ it 'returns a product' do
45
+ expect(RestClient).to receive(:get).exactly(1).times.with(url).and_return(response)
46
+ response = AliExpress::Affiliate.similar(12345)
47
+ expect(response['errorCode']).to eq(success)
48
+ end
49
+ end
50
+
51
+ describe '.popular' do
52
+ let(:url) { 'https://gw.api.alibaba.com/openapi/param2/2/portals.open/api.listHotProducts/fake-client-id?categoryId=3&language=en&localCurrency=USD' }
53
+
54
+ context 'when a category id is used' do
55
+ it 'returns a successful response' do
56
+ expect(RestClient).to receive(:get).exactly(1).times.with(url).and_return(response)
57
+ response = AliExpress::Affiliate.popular(3)
58
+ expect(response['errorCode']).to eq(success)
59
+ end
60
+ end
61
+
62
+ context 'when a category name is used' do
63
+ it 'returns a successful response' do
64
+ expect(RestClient).to receive(:get).exactly(1).times.with(url).and_return(response)
65
+ response = AliExpress::Affiliate.popular('Apparel & Accessories')
66
+ expect(response['errorCode']).to eq(success)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,94 @@
1
+ require 'spec_helper'
2
+
3
+ # rspec spec/aliexpress_spec.rb
4
+ describe AliExpress do
5
+ describe '.client_id' do
6
+ subject { AliExpress.client_id }
7
+ it { is_expected.to eq('fake-client-id') }
8
+
9
+ context('when assigned') do
10
+ before { AliExpress.client_id = 'foo' }
11
+ it { is_expected.to eq('foo') }
12
+ end
13
+ end
14
+
15
+ describe '.client_secret' do
16
+ subject { AliExpress.client_secret }
17
+ it { is_expected.to eq('fake-client-secret') }
18
+
19
+ context('when assigned') do
20
+ before { AliExpress.client_secret = 'foo' }
21
+ it { is_expected.to eq('foo') }
22
+ end
23
+ end
24
+
25
+ describe '.access_token' do
26
+ subject { AliExpress.access_token }
27
+ it { is_expected.to eq('fake-access-token') }
28
+
29
+ context('when assigned') do
30
+ before { AliExpress.access_token = 'foo' }
31
+ it { is_expected.to eq('foo') }
32
+ end
33
+ end
34
+
35
+ describe '.refresh_token' do
36
+ subject { AliExpress.refresh_token }
37
+ it { is_expected.to eq('fake-refresh-token') }
38
+
39
+ context('when assigned') do
40
+ before { AliExpress.refresh_token = 'foo' }
41
+ it { is_expected.to eq('foo') }
42
+ end
43
+ end
44
+
45
+ describe '.protocol' do
46
+ subject { AliExpress.protocol }
47
+ it { is_expected.to eq('https') }
48
+
49
+ context('when assigned') do
50
+ before { AliExpress.protocol = 'foo' }
51
+ it { is_expected.to eq('foo') }
52
+ end
53
+ end
54
+
55
+ describe '.host' do
56
+ subject { AliExpress.host }
57
+ it { is_expected.to eq('gw.api.alibaba.com') }
58
+
59
+ context('when assigned') do
60
+ before { AliExpress.host = 'foo' }
61
+ it { is_expected.to eq('foo') }
62
+ end
63
+ end
64
+
65
+ describe '.base_uri' do
66
+ subject { AliExpress.base_uri }
67
+ it { is_expected.to eq('https://gw.api.alibaba.com/openapi') }
68
+
69
+ context('when assigned') do
70
+ before { AliExpress.base_uri = 'foo' }
71
+ it { is_expected.to eq('foo') }
72
+ end
73
+ end
74
+
75
+ describe '.currency' do
76
+ subject { AliExpress.currency }
77
+ it { is_expected.to eq('USD') }
78
+
79
+ context('when assigned') do
80
+ before { AliExpress.currency = 'foo' }
81
+ it { is_expected.to eq('foo') }
82
+ end
83
+ end
84
+
85
+ describe '.language' do
86
+ subject { AliExpress.language }
87
+ it { is_expected.to eq('en') }
88
+
89
+ context('when assigned') do
90
+ before { AliExpress.language = 'foo' }
91
+ it { is_expected.to eq('foo') }
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ # rspec spec/auth_spec.rb
4
+ describe AliExpress::Auth do
5
+ describe '.authorize' do
6
+ let(:authorization_code) { 'b5ce38e1-833c-4f15-bd29-65e85db9bd2e' }
7
+ let(:response) do
8
+ {
9
+ 'refresh_token_timeout' => '20180402143144000-0700',
10
+ 'aliId' => 'fake-ali-id',
11
+ 'resource_owner' => 'fake-resource-owner',
12
+ access_token: 'new-fake-access-token',
13
+ refresh_token: 'new-fake-refresh-token',
14
+ expires_at: 1507266354
15
+ }
16
+ end
17
+
18
+ it 'creates a new access token' do
19
+ stub_const('RUBY_PLATFORM', 'test')
20
+ allow($stdin).to receive(:gets).and_return(authorization_code)
21
+ allow($stdout).to receive(:puts).and_return(nil)
22
+ allow($stdout).to receive(:print).and_return(nil)
23
+ expect_any_instance_of(OAuth2::Client).to receive(:get_token).exactly(1).times.with(hash_including(code: authorization_code)).and_return(response)
24
+ expect(AliExpress::Auth.authorize).to eq(response)
25
+ end
26
+ end
27
+
28
+ describe '.refresh' do
29
+ let(:response) do
30
+ mock = double
31
+ allow(mock).to receive(:code).and_return(200)
32
+ allow(mock).to receive(:body).and_return('{"aliId":"fake-ali-id","resource_owner":"fake-resource-owner","expires_in":"36000","access_token":"jabber_wocky"}')
33
+ mock
34
+ end
35
+
36
+ it 'exchanges the refresh token for a new access token' do
37
+ expect(RestClient).to receive(:post).exactly(1).times.and_return(response)
38
+ expect(AliExpress::Auth.refresh).to eq('jabber_wocky')
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,49 @@
1
+ require 'spec_helper'
2
+
3
+ # rspec spec/base_spec.rb
4
+ describe AliExpress::Base do
5
+ let(:response) do
6
+ mock = double
7
+ allow(mock).to receive(:code).and_return(200)
8
+ allow(mock).to receive(:body).and_return('{}')
9
+ mock
10
+ end
11
+
12
+ describe '.get' do
13
+ context 'when auth is true' do
14
+ it 'uses the access token' do
15
+ expect(RestClient).to receive(:get).exactly(1).times.with('https://gw.api.alibaba.com/openapi/param2/1/aliexpress.open/foo/fake-client-id?access_token=fake-access-token').and_return(response)
16
+ AliExpress::Base.get(api_call: 'foo', sign: false, auth: true)
17
+ end
18
+ end
19
+
20
+ context 'when sign is true' do
21
+ it 'signs the request' do
22
+ expect(RestClient).to receive(:get).exactly(1).times.with('https://gw.api.alibaba.com/openapi/param2/1/aliexpress.open/foo/fake-client-id?_aop_signature=8F19FBA00C4BF35E43FA9ED143639337047624C0').and_return(response)
23
+ AliExpress::Base.get(api_call: 'foo', sign: true, auth: false)
24
+ end
25
+ end
26
+
27
+ context 'when bogus params are used' do
28
+ it 'raises an error' do
29
+ expect { AliExpress::Base.get(api_call: 'foo', foo: 'bar') }.to raise_error(ArgumentError)
30
+ end
31
+ end
32
+ end
33
+
34
+ describe '.post' do
35
+ context 'when auth is true' do
36
+ it 'uses the access token' do
37
+ expect(RestClient).to receive(:post).exactly(1).times.with('https://gw.api.alibaba.com/openapi/param2/1/aliexpress.open/foo/fake-client-id', access_token: 'fake-access-token').and_return(response)
38
+ AliExpress::Base.post(api_call: 'foo', sign: false, auth: true)
39
+ end
40
+ end
41
+
42
+ context 'when sign is true' do
43
+ it 'signs the request' do
44
+ expect(RestClient).to receive(:post).exactly(1).times.with('https://gw.api.alibaba.com/openapi/param2/1/aliexpress.open/foo/fake-client-id', _aop_signature: '8F19FBA00C4BF35E43FA9ED143639337047624C0').and_return(response)
45
+ AliExpress::Base.post(api_call: 'foo', sign: true, auth: false)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,8 @@
1
+ require 'coveralls'
2
+ Coveralls.wear!
3
+
4
+ require 'aliexpress'
5
+ require 'awesome_print'
6
+
7
+ gemspec = Gem::Specification.find_by_name('aliexpress')
8
+ Dir["#{gemspec.gem_dir}/spec/support/**/*.rb"].each { |f| require f }
@@ -0,0 +1,12 @@
1
+ RSpec.configure do |config|
2
+ config.before(:each) do
3
+ AliExpress.client_id = 'fake-client-id'
4
+ AliExpress.client_secret = 'fake-client-secret'
5
+ AliExpress.access_token = 'fake-access-token'
6
+ AliExpress.refresh_token = 'fake-refresh-token'
7
+
8
+ %i(protocol host base_uri currency language logger).each do |method|
9
+ AliExpress.send("#{method}=", nil)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ require 'webmock/rspec'
2
+
3
+ WebMock.disable_net_connect!(allow_localhost: true)
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: aliexpress
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.4'
5
+ platform: ruby
6
+ authors:
7
+ - Chris Estreich
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-10-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rest-client
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: addressable
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: oauth2
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: AliExpress SDK for ruby.
56
+ email:
57
+ - chris@tophatter.com
58
+ executables:
59
+ - console
60
+ extensions: []
61
+ extra_rdoc_files:
62
+ - README.md
63
+ files:
64
+ - ".coveralls.yml"
65
+ - ".gitignore"
66
+ - ".rspec"
67
+ - ".rubocop.yml"
68
+ - ".travis.yml"
69
+ - Gemfile
70
+ - LICENSE.md
71
+ - README.md
72
+ - Rakefile
73
+ - aliexpress.gemspec
74
+ - bin/console
75
+ - lib/aliexpress.rb
76
+ - lib/aliexpress/affiliate.rb
77
+ - lib/aliexpress/auth.rb
78
+ - lib/aliexpress/base.rb
79
+ - lib/aliexpress/product.rb
80
+ - lib/aliexpress/response_error.rb
81
+ - spec/affiliate_spec.rb
82
+ - spec/aliexpress_spec.rb
83
+ - spec/auth_spec.rb
84
+ - spec/base_spec.rb
85
+ - spec/spec_helper.rb
86
+ - spec/support/auth.rb
87
+ - spec/support/webmock.rb
88
+ homepage: https://github.com/tophatter/aliexpress-api-ruby
89
+ licenses:
90
+ - MIT
91
+ metadata: {}
92
+ post_install_message: 'Documentation is available at: http://isvus.aliexpress.com/isv/index.htm'
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - "~>"
99
+ - !ruby/object:Gem::Version
100
+ version: '2.0'
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubyforge_project:
108
+ rubygems_version: 2.6.12
109
+ signing_key:
110
+ specification_version: 4
111
+ summary: AliExpress SDK for ruby.
112
+ test_files:
113
+ - spec/affiliate_spec.rb
114
+ - spec/aliexpress_spec.rb
115
+ - spec/auth_spec.rb
116
+ - spec/base_spec.rb
117
+ - spec/spec_helper.rb
118
+ - spec/support/auth.rb
119
+ - spec/support/webmock.rb