ebay_request 0.2.0
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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.rubocop.yml +23 -0
- data/.travis.yml +4 -0
- data/Gemfile +5 -0
- data/README.md +124 -0
- data/Rakefile +7 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/config/sites.yml +321 -0
- data/ebay_request.gemspec +46 -0
- data/lib/ebay_request.rb +77 -0
- data/lib/ebay_request/auth.rb +35 -0
- data/lib/ebay_request/base.rb +115 -0
- data/lib/ebay_request/business_policies.rb +39 -0
- data/lib/ebay_request/config.rb +59 -0
- data/lib/ebay_request/error.rb +9 -0
- data/lib/ebay_request/finding.rb +36 -0
- data/lib/ebay_request/response.rb +40 -0
- data/lib/ebay_request/shopping.rb +36 -0
- data/lib/ebay_request/site.rb +30 -0
- data/lib/ebay_request/trading.rb +83 -0
- data/lib/ebay_request/version.rb +4 -0
- data/lib/omniauth/strategies/ebay.rb +80 -0
- metadata +208 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
lib = File.expand_path("../lib", __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require "ebay_request/version"
|
6
|
+
|
7
|
+
# rubocop:disable Metrics/BlockLength
|
8
|
+
Gem::Specification.new do |spec|
|
9
|
+
spec.name = "ebay_request"
|
10
|
+
spec.version = EbayRequest::VERSION
|
11
|
+
spec.authors = ["Victor Sokolov"]
|
12
|
+
spec.email = ["gzigzigzeo@evilmartians.com"]
|
13
|
+
|
14
|
+
spec.summary = "eBay API request interface"
|
15
|
+
# spec.description = "TODO: Write a longer description or delete this line."
|
16
|
+
spec.homepage = "https://github.com/gzigzigzeo/ebay_request"
|
17
|
+
|
18
|
+
# Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
|
19
|
+
# delete this section to allow pushing this gem to any host.
|
20
|
+
if spec.respond_to?(:metadata)
|
21
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
22
|
+
else
|
23
|
+
raise "RubyGems 2.0 or newer is required to protect against public \
|
24
|
+
gem pushes."
|
25
|
+
end
|
26
|
+
|
27
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
28
|
+
f.match(%r{^(test|spec|features)/})
|
29
|
+
end
|
30
|
+
spec.bindir = "exe"
|
31
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
32
|
+
spec.require_paths = ["lib"]
|
33
|
+
|
34
|
+
spec.add_development_dependency "bundler", "~> 1.11"
|
35
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
36
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
37
|
+
spec.add_development_dependency "rubocop"
|
38
|
+
spec.add_development_dependency "webmock"
|
39
|
+
spec.add_development_dependency "simplecov"
|
40
|
+
|
41
|
+
spec.add_dependency "multi_xml"
|
42
|
+
spec.add_dependency "gyoku"
|
43
|
+
spec.add_dependency "omniauth"
|
44
|
+
spec.add_dependency "dry-initializer"
|
45
|
+
end
|
46
|
+
# rubocop:enable Metrics/BlockLength
|
data/lib/ebay_request.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "omniauth"
|
3
|
+
require "net/http"
|
4
|
+
require "gyoku"
|
5
|
+
require "multi_xml"
|
6
|
+
require "dry-initializer"
|
7
|
+
|
8
|
+
require "ebay_request/version"
|
9
|
+
require "ebay_request/site"
|
10
|
+
require "ebay_request/config"
|
11
|
+
require "ebay_request/base"
|
12
|
+
require "ebay_request/error"
|
13
|
+
require "ebay_request/finding"
|
14
|
+
require "ebay_request/shopping"
|
15
|
+
require "ebay_request/trading"
|
16
|
+
require "ebay_request/business_policies"
|
17
|
+
require "ebay_request/auth"
|
18
|
+
require "ebay_request/response"
|
19
|
+
|
20
|
+
require "omniauth/strategies/ebay"
|
21
|
+
|
22
|
+
module EbayRequest
|
23
|
+
class << self
|
24
|
+
attr_accessor :logger
|
25
|
+
attr_accessor :warn_logger
|
26
|
+
attr_accessor :config_repository
|
27
|
+
attr_accessor :json_logger
|
28
|
+
|
29
|
+
def config(key = nil)
|
30
|
+
@config_repository ||= {}
|
31
|
+
@config_repository[key || :default] ||= Config.new
|
32
|
+
end
|
33
|
+
|
34
|
+
def configure(key = nil)
|
35
|
+
yield(config(key)) && config(key)
|
36
|
+
end
|
37
|
+
|
38
|
+
def configured?
|
39
|
+
!@config_repository.nil?
|
40
|
+
end
|
41
|
+
|
42
|
+
def log(options)
|
43
|
+
log_info(options)
|
44
|
+
log_warn(options)
|
45
|
+
log_json(options)
|
46
|
+
end
|
47
|
+
|
48
|
+
# rubocop:disable Metrics/AbcSize
|
49
|
+
def log_info(o)
|
50
|
+
return if logger.nil?
|
51
|
+
logger.info "[EbayRequest] | Url | #{o[:url]}"
|
52
|
+
logger.info "[EbayRequest] | Headers | #{o[:headers]}"
|
53
|
+
logger.info "[EbayRequest] | Body | #{o[:request_payload]}"
|
54
|
+
logger.info "[EbayRequest] | Response | #{fix_utf(o[:response_payload])}"
|
55
|
+
logger.info "[EbayRequest] | Time | #{o[:time]} #{o[:callname]}"
|
56
|
+
end
|
57
|
+
# rubocop:enable Metrics/AbcSize
|
58
|
+
|
59
|
+
def log_warn(o)
|
60
|
+
return if warn_logger.nil? || o[:warnings].empty?
|
61
|
+
warn_logger.warn(
|
62
|
+
"[EbayRequest] | #{o[:callname]} | #{o[:warnings].inspect}"
|
63
|
+
)
|
64
|
+
end
|
65
|
+
|
66
|
+
def log_json(options)
|
67
|
+
return if json_logger.nil?
|
68
|
+
json_logger.info(options)
|
69
|
+
end
|
70
|
+
|
71
|
+
def fix_utf(response)
|
72
|
+
response.encode(
|
73
|
+
"UTF-8", undef: :replace, invalid: :replace, replace: " "
|
74
|
+
)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class EbayRequest::Auth < EbayRequest::Trading
|
3
|
+
def session_id
|
4
|
+
response!("GetSessionID", RuName: config.runame)
|
5
|
+
end
|
6
|
+
|
7
|
+
def token(session_id)
|
8
|
+
response!("FetchToken", SessionID: session_id)
|
9
|
+
end
|
10
|
+
|
11
|
+
def user(auth_token)
|
12
|
+
response!("GetUser", RequesterCredentials: { eBayAuthToken: auth_token },
|
13
|
+
DetailLevel: "ReturnAll")
|
14
|
+
end
|
15
|
+
|
16
|
+
def ebay_login_url(session_id, ruparams = {})
|
17
|
+
params = [
|
18
|
+
"SignIn",
|
19
|
+
"RuName=#{CGI.escape(config.runame)}",
|
20
|
+
"SessID=#{CGI.escape(session_id)}",
|
21
|
+
]
|
22
|
+
ruparams = CGI.escape(ruparams.map { |k, v| "#{k}=#{v}" }.join("&"))
|
23
|
+
params << "ruparams=#{CGI.escape(ruparams)}"
|
24
|
+
|
25
|
+
"#{signin_endpoint}?#{params.join('&')}"
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def signin_endpoint
|
31
|
+
URI.parse(
|
32
|
+
with_sandbox("https://signin%{sandbox}.ebay.com/ws/eBayISAPI.dll")
|
33
|
+
)
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class EbayRequest::Base
|
3
|
+
def initialize(options = {})
|
4
|
+
@options = options
|
5
|
+
end
|
6
|
+
|
7
|
+
attr_reader :options
|
8
|
+
|
9
|
+
def config
|
10
|
+
@config ||= EbayRequest.config(options[:env])
|
11
|
+
end
|
12
|
+
|
13
|
+
def siteid
|
14
|
+
@siteid ||=
|
15
|
+
options[:siteid] ||
|
16
|
+
EbayRequest::Config.site_id_from_globalid(options[:globalid]) ||
|
17
|
+
0
|
18
|
+
end
|
19
|
+
|
20
|
+
def globalid
|
21
|
+
@globalid ||=
|
22
|
+
options[:globalid] ||
|
23
|
+
EbayRequest::Config.globalid_from_site_id(options[:siteid]) ||
|
24
|
+
"EBAY-US"
|
25
|
+
end
|
26
|
+
|
27
|
+
def response(callname, payload)
|
28
|
+
config.validate!
|
29
|
+
request(URI.parse(with_sandbox(endpoint)), callname, payload)
|
30
|
+
end
|
31
|
+
|
32
|
+
def response!(callname, payload)
|
33
|
+
response(callname, payload).data!
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def endpoint
|
39
|
+
raise NotImplementedError, "Implement #{self.class.name}#endpoint"
|
40
|
+
end
|
41
|
+
|
42
|
+
def headers(_callname)
|
43
|
+
@options[:headers] || {}
|
44
|
+
end
|
45
|
+
|
46
|
+
def payload(_callname, _request)
|
47
|
+
raise NotImplementedError, "Implement #{self.class.name}#payload"
|
48
|
+
end
|
49
|
+
|
50
|
+
def parse(response)
|
51
|
+
MultiXml.parse(response)
|
52
|
+
end
|
53
|
+
|
54
|
+
def process(response, callname)
|
55
|
+
data = response["#{callname}Response"]
|
56
|
+
|
57
|
+
raise EbayRequest::Error, "#{callname} response is blank" if data.nil?
|
58
|
+
EbayRequest::Response.new(
|
59
|
+
callname, data, errors_for(data), self.class::FATAL_ERRORS
|
60
|
+
)
|
61
|
+
end
|
62
|
+
|
63
|
+
def with_sandbox(value)
|
64
|
+
# rubocop:disable Style/FormatString
|
65
|
+
value % { sandbox: config.sandbox? ? ".sandbox" : "" }
|
66
|
+
end
|
67
|
+
|
68
|
+
# rubocop:disable Metrics/MethodLength
|
69
|
+
def request(url, callname, request)
|
70
|
+
h = headers(callname)
|
71
|
+
b = payload(callname, request)
|
72
|
+
|
73
|
+
post = Net::HTTP::Post.new(url.path, h)
|
74
|
+
post.body = b
|
75
|
+
|
76
|
+
response, time = make_request(url, post)
|
77
|
+
|
78
|
+
response_object = process(parse(response), callname)
|
79
|
+
ensure
|
80
|
+
EbayRequest.log(
|
81
|
+
url: url.to_s,
|
82
|
+
callname: callname,
|
83
|
+
headers: h,
|
84
|
+
request_payload: b,
|
85
|
+
response_payload: response,
|
86
|
+
time: time,
|
87
|
+
warnings: response_object&.warnings,
|
88
|
+
errors: response_object&.errors,
|
89
|
+
success: response_object&.success?
|
90
|
+
)
|
91
|
+
end
|
92
|
+
# rubocop:enable Metrics/MethodLength
|
93
|
+
|
94
|
+
def make_request(url, post)
|
95
|
+
start_time = Time.now
|
96
|
+
http = prepare(url)
|
97
|
+
response = http.start { |r| r.request(post) }.body
|
98
|
+
[response, Time.now - start_time]
|
99
|
+
end
|
100
|
+
|
101
|
+
def prepare(url)
|
102
|
+
Net::HTTP.new(url.host, url.port).tap do |http|
|
103
|
+
http.read_timeout = config.timeout
|
104
|
+
|
105
|
+
if url.port == 443
|
106
|
+
http.use_ssl = true
|
107
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def errors_for(_r)
|
113
|
+
raise NotImplementedError, "Implement #{self.class.name}#errors_for"
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class EbayRequest::BusinessPolicies < EbayRequest::Base
|
3
|
+
private
|
4
|
+
|
5
|
+
SERVICE_NAME = "SellerProfilesManagementService"
|
6
|
+
|
7
|
+
def payload(callname, request)
|
8
|
+
key_converter = ->(key) { key.camelize(:lower) }
|
9
|
+
request = Gyoku.xml(request, key_converter: key_converter)
|
10
|
+
|
11
|
+
%(<?xml version="1.0" encoding="utf-8"?>\
|
12
|
+
<#{callname}Request xmlns="http://www.ebay.com/marketplace/selling">\
|
13
|
+
#{request}</#{callname}Request>)
|
14
|
+
end
|
15
|
+
|
16
|
+
def endpoint
|
17
|
+
"https://svcs%{sandbox}.ebay.com/services/selling/v1/#{SERVICE_NAME}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def headers(callname)
|
21
|
+
super.merge(
|
22
|
+
"Content-Type" => "text/xml",
|
23
|
+
"X-EBAY-SOA-SECURITY-TOKEN" => options[:token].to_s,
|
24
|
+
"X-EBAY-SOA-SERVICE-NAME" => SERVICE_NAME,
|
25
|
+
"X-EBAY-SOA-OPERATION-NAME" => callname,
|
26
|
+
"X-EBAY-SOA-CONTENT-TYPE" => "XML",
|
27
|
+
"X-EBAY-SOA-GLOBAL-ID" => siteid.to_s
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
def errors_for(r)
|
32
|
+
[r.dig("errorMessage", "error")]
|
33
|
+
.flatten
|
34
|
+
.compact
|
35
|
+
.map { |e| [e["severity"], e["errorId"], e["message"]] }
|
36
|
+
end
|
37
|
+
|
38
|
+
FATAL_ERRORS = {}.freeze
|
39
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class EbayRequest::Config
|
3
|
+
attr_accessor :appid
|
4
|
+
attr_accessor :certid
|
5
|
+
attr_accessor :devid
|
6
|
+
attr_accessor :runame
|
7
|
+
|
8
|
+
attr_accessor :sandbox
|
9
|
+
attr_accessor :version
|
10
|
+
attr_accessor :timeout
|
11
|
+
|
12
|
+
alias sandbox? sandbox
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@sandbox ||= true
|
16
|
+
@version ||= 941
|
17
|
+
@timeout ||= 60
|
18
|
+
end
|
19
|
+
|
20
|
+
def validate!
|
21
|
+
%w(appid certid devid runame).each do |attr|
|
22
|
+
value = public_send(attr)
|
23
|
+
raise "Set EbayRequest.config.#{attr}" if value.nil? || value.empty?
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class << self
|
28
|
+
def globalid_from_site_id(site_id)
|
29
|
+
(site = sites_by_id[site_id]) && site.globalid
|
30
|
+
end
|
31
|
+
|
32
|
+
def site_id_from_globalid(globalid)
|
33
|
+
(site = sites_by_globalid[globalid.to_s.upcase]) && site.id
|
34
|
+
end
|
35
|
+
|
36
|
+
def site_id_from_name(name)
|
37
|
+
(site = sites_by_name[name]) && site.id
|
38
|
+
end
|
39
|
+
|
40
|
+
def sites
|
41
|
+
@sites ||=
|
42
|
+
YAML.load_file(
|
43
|
+
File.join(File.dirname(__FILE__), "../../config/sites.yml")
|
44
|
+
).map { |h| EbayRequest::Site.new h }
|
45
|
+
end
|
46
|
+
|
47
|
+
def sites_by_id
|
48
|
+
@sites_by_id ||= Hash[sites.map { |s| [s.id, s] }]
|
49
|
+
end
|
50
|
+
|
51
|
+
def sites_by_globalid
|
52
|
+
@sites_by_globalid ||= Hash[sites.map { |s| [s.globalid, s] }]
|
53
|
+
end
|
54
|
+
|
55
|
+
def sites_by_name
|
56
|
+
@sites_by_name ||= Hash[sites.map { |s| [s.name, s] }]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class EbayRequest::Finding < EbayRequest::Base
|
3
|
+
private
|
4
|
+
|
5
|
+
def payload(callname, request)
|
6
|
+
request = Gyoku.xml(request)
|
7
|
+
|
8
|
+
%(<?xml version="1.0" encoding="utf-8"?><#{callname}Request\
|
9
|
+
xmlns="http://www.ebay.com/marketplace/search/v1/services">\
|
10
|
+
#{request}</#{callname}Request>)
|
11
|
+
end
|
12
|
+
|
13
|
+
def endpoint
|
14
|
+
"http://svcs%{sandbox}.ebay.com/services/search/FindingService/v1"
|
15
|
+
end
|
16
|
+
|
17
|
+
def headers(callname)
|
18
|
+
super.merge(
|
19
|
+
"X-EBAY-SOA-SERVICE-NAME" => "FindingService",
|
20
|
+
"X-EBAY-SOA-SERVICE-VERSION" => "1.9.0",
|
21
|
+
"X-EBAY-SOA-SECURITY-APPNAME" => config.appid,
|
22
|
+
"X-EBAY-SOA-OPERATION-NAME" => callname,
|
23
|
+
"X-EBAY-SOA-REQUEST-DATA-FORMAT" => "XML",
|
24
|
+
"X-EBAY-SOA-GLOBAL-ID" => globalid.to_s
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
def errors_for(r)
|
29
|
+
[r.dig("errorMessage", "error")]
|
30
|
+
.flatten
|
31
|
+
.compact
|
32
|
+
.map { |e| [e["severity"], e["errorId"], e["message"]] }
|
33
|
+
end
|
34
|
+
|
35
|
+
FATAL_ERRORS = {}.freeze
|
36
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class EbayRequest::Response
|
3
|
+
extend Dry::Initializer
|
4
|
+
|
5
|
+
param :callname
|
6
|
+
param :data
|
7
|
+
param :errors_data
|
8
|
+
param :fatal_errors
|
9
|
+
|
10
|
+
def success?
|
11
|
+
ack = data["ack"] || data["Ack"]
|
12
|
+
%w(Success Warning).include?(ack)
|
13
|
+
end
|
14
|
+
|
15
|
+
def data!
|
16
|
+
raise error unless success?
|
17
|
+
data
|
18
|
+
end
|
19
|
+
|
20
|
+
def errors
|
21
|
+
severity("Error")
|
22
|
+
end
|
23
|
+
|
24
|
+
def warnings
|
25
|
+
severity("Warning")
|
26
|
+
end
|
27
|
+
|
28
|
+
def severity(severity)
|
29
|
+
Hash[errors_data.map { |s, c, m| [c.to_i, m] if s == severity }.compact]
|
30
|
+
end
|
31
|
+
|
32
|
+
def error_class
|
33
|
+
fatal_code = (errors.keys.map(&:to_i) & fatal_errors.keys).first
|
34
|
+
fatal_errors[fatal_code] || EbayRequest::Error
|
35
|
+
end
|
36
|
+
|
37
|
+
def error
|
38
|
+
error_class.new(errors.values.join(", "), errors)
|
39
|
+
end
|
40
|
+
end
|