tailored-etsy 0.2.2
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.
- data/.gitignore +8 -0
- data/.travis.yml +8 -0
- data/Gemfile +3 -0
- data/LICENSE +9 -0
- data/README.md +280 -0
- data/Rakefile +12 -0
- data/etsy.gemspec +28 -0
- data/lib/etsy.rb +172 -0
- data/lib/etsy/address.rb +47 -0
- data/lib/etsy/basic_client.rb +26 -0
- data/lib/etsy/category.rb +84 -0
- data/lib/etsy/country.rb +27 -0
- data/lib/etsy/image.rb +34 -0
- data/lib/etsy/listing.rb +178 -0
- data/lib/etsy/model.rb +123 -0
- data/lib/etsy/payment_template.rb +33 -0
- data/lib/etsy/profile.rb +49 -0
- data/lib/etsy/request.rb +148 -0
- data/lib/etsy/response.rb +112 -0
- data/lib/etsy/section.rb +16 -0
- data/lib/etsy/secure_client.rb +128 -0
- data/lib/etsy/shipping_template.rb +32 -0
- data/lib/etsy/shop.rb +83 -0
- data/lib/etsy/transaction.rb +18 -0
- data/lib/etsy/user.rb +91 -0
- data/lib/etsy/verification_request.rb +17 -0
- data/lib/etsy/version.rb +3 -0
- data/test/fixtures/address/getUserAddresses.json +12 -0
- data/test/fixtures/category/findAllSubCategoryChildren.json +78 -0
- data/test/fixtures/category/findAllTopCategory.json +347 -0
- data/test/fixtures/category/findAllTopCategory.single.json +18 -0
- data/test/fixtures/category/findAllTopCategoryChildren.json +308 -0
- data/test/fixtures/category/getCategory.multiple.json +28 -0
- data/test/fixtures/category/getCategory.single.json +18 -0
- data/test/fixtures/country/getCountry.json +1 -0
- data/test/fixtures/image/findAllListingImages.json +102 -0
- data/test/fixtures/listing/findAllListingActive.category.json +827 -0
- data/test/fixtures/listing/findAllShopListings.json +69 -0
- data/test/fixtures/listing/getListing.multiple.json +1 -0
- data/test/fixtures/listing/getListing.single.json +1 -0
- data/test/fixtures/payment_template/getPaymentTemplate.json +1 -0
- data/test/fixtures/profile/new.json +28 -0
- data/test/fixtures/section/getShopSection.json +18 -0
- data/test/fixtures/shipping_template/getShippingTemplate.json +1 -0
- data/test/fixtures/shop/findAllShop.json +1 -0
- data/test/fixtures/shop/findAllShop.single.json +1 -0
- data/test/fixtures/shop/getShop.multiple.json +1 -0
- data/test/fixtures/shop/getShop.single.json +33 -0
- data/test/fixtures/transaction/findAllShopTransactions.json +1 -0
- data/test/fixtures/user/getUser.multiple.json +29 -0
- data/test/fixtures/user/getUser.single.json +13 -0
- data/test/fixtures/user/getUser.single.private.json +18 -0
- data/test/fixtures/user/getUser.single.withProfile.json +38 -0
- data/test/fixtures/user/getUser.single.withShops.json +41 -0
- data/test/test_helper.rb +44 -0
- data/test/unit/etsy/address_test.rb +61 -0
- data/test/unit/etsy/basic_client_test.rb +28 -0
- data/test/unit/etsy/category_test.rb +106 -0
- data/test/unit/etsy/country_test.rb +64 -0
- data/test/unit/etsy/image_test.rb +43 -0
- data/test/unit/etsy/listing_test.rb +217 -0
- data/test/unit/etsy/model_test.rb +64 -0
- data/test/unit/etsy/payment_template_test.rb +68 -0
- data/test/unit/etsy/profile_test.rb +111 -0
- data/test/unit/etsy/request_test.rb +192 -0
- data/test/unit/etsy/response_test.rb +164 -0
- data/test/unit/etsy/section_test.rb +28 -0
- data/test/unit/etsy/secure_client_test.rb +132 -0
- data/test/unit/etsy/shipping_template_test.rb +24 -0
- data/test/unit/etsy/shop_test.rb +104 -0
- data/test/unit/etsy/transaction_test.rb +52 -0
- data/test/unit/etsy/user_test.rb +218 -0
- data/test/unit/etsy/verification_request_test.rb +26 -0
- data/test/unit/etsy_test.rb +114 -0
- metadata +269 -0
@@ -0,0 +1,33 @@
|
|
1
|
+
module Etsy
|
2
|
+
class PaymentTemplate
|
3
|
+
include Etsy::Model
|
4
|
+
|
5
|
+
attribute :id, :from => :payment_template_id
|
6
|
+
attributes :allow_check, :allow_mo, :allow_other, :allow_paypal, :allow_cc
|
7
|
+
attributes :paypal_email, :name, :first_line, :second_line, :city, :state
|
8
|
+
attributes :zip, :country_id
|
9
|
+
|
10
|
+
def self.create(options = {})
|
11
|
+
options.merge!(:require_secure => true)
|
12
|
+
post("/payments/templates", options)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.find(id, credentials = {})
|
16
|
+
options = {
|
17
|
+
:access_token => credentials[:access_token],
|
18
|
+
:access_secret => credentials[:access_secret],
|
19
|
+
:require_secure => true
|
20
|
+
}
|
21
|
+
get("/payments/templates/#{id}", options)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.find_by_user(user, credentials = {})
|
25
|
+
options = {
|
26
|
+
:access_token => credentials[:access_token],
|
27
|
+
:access_secret => credentials[:access_secret],
|
28
|
+
:require_secure => true
|
29
|
+
}
|
30
|
+
get("/users/#{user.id}/payments/templates", options)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/etsy/profile.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
module Etsy
|
2
|
+
|
3
|
+
# = Profile
|
4
|
+
#
|
5
|
+
# Represents a profile resource of an Etsy user.
|
6
|
+
#
|
7
|
+
class Profile
|
8
|
+
|
9
|
+
include Etsy::Model
|
10
|
+
|
11
|
+
attribute :id, :from => :user_profile_id
|
12
|
+
attribute :user_id
|
13
|
+
attribute :bio
|
14
|
+
attribute :username, :from => :login_name
|
15
|
+
attribute :gender
|
16
|
+
attribute :birth_day
|
17
|
+
attribute :birth_month
|
18
|
+
attribute :birth_year
|
19
|
+
attribute :joined, :from => :join_tsz
|
20
|
+
attribute :favorite_materials, :from => :materials
|
21
|
+
attribute :country_id
|
22
|
+
attribute :city
|
23
|
+
attribute :location
|
24
|
+
attribute :region
|
25
|
+
attribute :avatar_id
|
26
|
+
attribute :image, :from => :image_url_75x75
|
27
|
+
attribute :lat
|
28
|
+
attribute :lon
|
29
|
+
attribute :transaction_buy_count
|
30
|
+
attribute :transaction_sold_count
|
31
|
+
attribute :is_seller
|
32
|
+
attribute :first_name
|
33
|
+
attribute :last_name
|
34
|
+
|
35
|
+
def materials
|
36
|
+
favorite_materials ? favorite_materials.split(',') : []
|
37
|
+
end
|
38
|
+
|
39
|
+
# Time that this user joined Etsy
|
40
|
+
#
|
41
|
+
def joined_at
|
42
|
+
Time.at(joined)
|
43
|
+
end
|
44
|
+
|
45
|
+
def seller?
|
46
|
+
is_seller
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/etsy/request.rb
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
module Etsy
|
2
|
+
|
3
|
+
# = Request
|
4
|
+
#
|
5
|
+
# A basic wrapper around GET requests to the Etsy JSON API
|
6
|
+
#
|
7
|
+
class Request
|
8
|
+
|
9
|
+
# Perform a GET request for the resource with optional parameters - returns
|
10
|
+
# A Response object with the payload data
|
11
|
+
def self.get(resource_path, parameters = {})
|
12
|
+
request = Request.new(resource_path, parameters)
|
13
|
+
Response.new(request.get)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.post(resource_path, parameters = {})
|
17
|
+
request = Request.new(resource_path, parameters)
|
18
|
+
Response.new(request.post)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.put(resource_path, parameters = {})
|
22
|
+
request = Request.new(resource_path, parameters)
|
23
|
+
Response.new(request.put)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.delete(resource_path, parameters = {})
|
27
|
+
request = Request.new(resource_path, parameters)
|
28
|
+
Response.new(request.delete)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Create a new request for the resource with optional parameters
|
32
|
+
def initialize(resource_path, parameters = {})
|
33
|
+
parameters = parameters.dup
|
34
|
+
@token = parameters.delete(:access_token) || Etsy.credentials[:access_token]
|
35
|
+
@secret = parameters.delete(:access_secret) || Etsy.credentials[:access_secret]
|
36
|
+
raise("Secure connection required. Please provide your OAuth credentials via :access_token and :access_secret in the parameters") if parameters.delete(:require_secure) && !secure?
|
37
|
+
@multipart_request = parameters.delete(:multipart)
|
38
|
+
@resource_path = resource_path
|
39
|
+
@resources = parameters.delete(:includes)
|
40
|
+
if @resources.class == String
|
41
|
+
@resources = @resources.split(',').map {|r| {:resource => r}}
|
42
|
+
elsif @resources.class == Array
|
43
|
+
@resources = @resources.map do |r|
|
44
|
+
if r.class == String
|
45
|
+
{:resource => r}
|
46
|
+
else
|
47
|
+
r
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
parameters = parameters.merge(:api_key => Etsy.api_key) unless secure?
|
52
|
+
@parameters = parameters
|
53
|
+
end
|
54
|
+
|
55
|
+
def base_path # :nodoc:
|
56
|
+
"/v2"
|
57
|
+
end
|
58
|
+
|
59
|
+
# Perform a GET request against the API endpoint and return the raw
|
60
|
+
# response data
|
61
|
+
def get
|
62
|
+
client.get(endpoint_url)
|
63
|
+
end
|
64
|
+
|
65
|
+
def post
|
66
|
+
if multipart?
|
67
|
+
client.post_multipart(endpoint_url(:include_query => false), @parameters)
|
68
|
+
else
|
69
|
+
client.post(endpoint_url)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def put
|
74
|
+
client.put(endpoint_url)
|
75
|
+
end
|
76
|
+
|
77
|
+
def delete
|
78
|
+
client.delete(endpoint_url)
|
79
|
+
end
|
80
|
+
|
81
|
+
def client # :nodoc:
|
82
|
+
@client ||= secure? ? secure_client : basic_client
|
83
|
+
end
|
84
|
+
|
85
|
+
def parameters # :nodoc:
|
86
|
+
@parameters
|
87
|
+
end
|
88
|
+
|
89
|
+
def resources # :nodoc:
|
90
|
+
@resources
|
91
|
+
end
|
92
|
+
|
93
|
+
def query # :nodoc:
|
94
|
+
to_url(parameters.merge(:includes => resources.to_a.map { |r| association(r) }))
|
95
|
+
end
|
96
|
+
|
97
|
+
def to_url(val)
|
98
|
+
if val.is_a? Array
|
99
|
+
to_url(val.join(','))
|
100
|
+
elsif val.is_a? Hash
|
101
|
+
val.reject { |k, v|
|
102
|
+
k.nil? || v.nil? || (k.respond_to?(:empty?) && k.empty?) || (v.respond_to?(:empty?) && v.empty?)
|
103
|
+
}.map { |k, v| "#{to_url(k.to_s)}=#{to_url(v)}" }.join('&')
|
104
|
+
else
|
105
|
+
URI.escape(val.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def association(options={}) # :nodoc:
|
110
|
+
s = options[:resource].dup
|
111
|
+
|
112
|
+
if options.include? :fields
|
113
|
+
s << "(#{[options[:fields]].flatten.join(',')})"
|
114
|
+
end
|
115
|
+
|
116
|
+
if options.include?(:limit) || options.include?(:offset)
|
117
|
+
s << ":#{options.fetch(:limit, 25)}:#{options.fetch(:offset, 0)}"
|
118
|
+
end
|
119
|
+
|
120
|
+
s
|
121
|
+
end
|
122
|
+
|
123
|
+
def endpoint_url(options = {}) # :nodoc:
|
124
|
+
url = "#{base_path}#{@resource_path}"
|
125
|
+
url += "?#{query}" if options.fetch(:include_query, true)
|
126
|
+
url
|
127
|
+
end
|
128
|
+
|
129
|
+
def multipart?
|
130
|
+
!!@multipart_request
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
def secure_client
|
136
|
+
SecureClient.new(:access_token => @token, :access_secret => @secret)
|
137
|
+
end
|
138
|
+
|
139
|
+
def basic_client
|
140
|
+
BasicClient.new(Etsy.host)
|
141
|
+
end
|
142
|
+
|
143
|
+
def secure?
|
144
|
+
!@token.nil? && !@secret.nil?
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module Etsy
|
2
|
+
|
3
|
+
class OAuthTokenRevoked < StandardError; end
|
4
|
+
class MissingShopID < StandardError; end
|
5
|
+
class EtsyJSONInvalid < StandardError; end
|
6
|
+
class TemporaryIssue < StandardError; end
|
7
|
+
class InvalidUserID < StandardError; end
|
8
|
+
|
9
|
+
# = Response
|
10
|
+
#
|
11
|
+
# Basic wrapper around the Etsy JSON response data
|
12
|
+
#
|
13
|
+
class Response
|
14
|
+
|
15
|
+
# Create a new response based on the raw HTTP response
|
16
|
+
def initialize(raw_response)
|
17
|
+
@raw_response = raw_response
|
18
|
+
end
|
19
|
+
|
20
|
+
# Convert the raw JSON data to a hash
|
21
|
+
def to_hash
|
22
|
+
validate!
|
23
|
+
@hash ||= json
|
24
|
+
end
|
25
|
+
|
26
|
+
def body
|
27
|
+
@raw_response.body
|
28
|
+
end
|
29
|
+
|
30
|
+
def code
|
31
|
+
@raw_response.code
|
32
|
+
end
|
33
|
+
|
34
|
+
# Number of records in the response results
|
35
|
+
def count
|
36
|
+
if paginated?
|
37
|
+
to_hash['results'].nil? ? 0 : to_hash['results'].size
|
38
|
+
else
|
39
|
+
to_hash['count']
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Results of the API request
|
44
|
+
def result
|
45
|
+
if success?
|
46
|
+
results = to_hash['results'] || []
|
47
|
+
count == 1 ? results.first : results
|
48
|
+
else
|
49
|
+
[]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def success?
|
54
|
+
!!(code =~ /2\d\d/)
|
55
|
+
end
|
56
|
+
|
57
|
+
def paginated?
|
58
|
+
!!to_hash['pagination']
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def data
|
64
|
+
@raw_response.body
|
65
|
+
end
|
66
|
+
|
67
|
+
def json
|
68
|
+
@hash ||= JSON.parse(data)
|
69
|
+
end
|
70
|
+
|
71
|
+
def validate!
|
72
|
+
raise OAuthTokenRevoked if token_revoked?
|
73
|
+
raise MissingShopID if missing_shop_id?
|
74
|
+
raise InvalidUserID if invalid_user_id?
|
75
|
+
raise TemporaryIssue if temporary_etsy_issue? || resource_unavailable? || exceeded_rate_limit?
|
76
|
+
raise EtsyJSONInvalid.new("CODE: #{code}, BODY: #{data}") unless valid_json?
|
77
|
+
true
|
78
|
+
end
|
79
|
+
|
80
|
+
def valid_json?
|
81
|
+
json
|
82
|
+
return true
|
83
|
+
rescue JSON::ParserError
|
84
|
+
return false
|
85
|
+
end
|
86
|
+
|
87
|
+
def token_revoked?
|
88
|
+
data == "oauth_problem=token_revoked"
|
89
|
+
end
|
90
|
+
|
91
|
+
def missing_shop_id?
|
92
|
+
data =~ /Shop with PK shop_id/
|
93
|
+
end
|
94
|
+
|
95
|
+
def invalid_user_id?
|
96
|
+
data =~ /is not a valid user_id/
|
97
|
+
end
|
98
|
+
|
99
|
+
def temporary_etsy_issue?
|
100
|
+
data =~ /Temporary Etsy issue/
|
101
|
+
end
|
102
|
+
|
103
|
+
def resource_unavailable?
|
104
|
+
data =~ /Resource temporarily unavailable/
|
105
|
+
end
|
106
|
+
|
107
|
+
def exceeded_rate_limit?
|
108
|
+
data =~ /You have exceeded/
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
end
|
data/lib/etsy/section.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
module Etsy
|
2
|
+
class Section
|
3
|
+
include Etsy::Model
|
4
|
+
|
5
|
+
attributes :title, :rank, :user_id, :active_listing_count
|
6
|
+
attribute :id, :from => :shop_section_id
|
7
|
+
|
8
|
+
def self.find_by_shop(shop)
|
9
|
+
get("/shops/#{shop.id}/sections")
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.find(shop, id)
|
13
|
+
get("/shops/#{shop.id}/sections/#{id}")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
module Etsy
|
2
|
+
|
3
|
+
# = SecureClient
|
4
|
+
#
|
5
|
+
# Used for generating tokens and calling API methods that require authentication.
|
6
|
+
#
|
7
|
+
class SecureClient
|
8
|
+
|
9
|
+
# Create a new client with the necessary parameters. Accepts the following
|
10
|
+
# key/value pairs:
|
11
|
+
#
|
12
|
+
# :request_token
|
13
|
+
# :request_secret
|
14
|
+
# :access_token
|
15
|
+
# :access_secret
|
16
|
+
#
|
17
|
+
# The request token / secret is useful for generating the access token. Pass
|
18
|
+
# the access token / secret when initializing from stored credentials.
|
19
|
+
#
|
20
|
+
def initialize(attributes = {})
|
21
|
+
@attributes = attributes
|
22
|
+
end
|
23
|
+
|
24
|
+
def consumer # :nodoc:
|
25
|
+
path = '/v2/oauth/'
|
26
|
+
@consumer ||= OAuth::Consumer.new(Etsy.api_key, Etsy.api_secret, {
|
27
|
+
:site => "http://#{Etsy.host}",
|
28
|
+
:request_token_path => "#{path}request_token?scope=#{Etsy.permission_scopes.join('+')}",
|
29
|
+
:access_token_path => "#{path}access_token"
|
30
|
+
})
|
31
|
+
end
|
32
|
+
|
33
|
+
# Generate a request token.
|
34
|
+
#
|
35
|
+
def request_token
|
36
|
+
consumer.get_request_token(:oauth_callback => Etsy.callback_url)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Access token for this request, either generated from the request token or taken
|
40
|
+
# from the :access_token parameter.
|
41
|
+
#
|
42
|
+
def access_token
|
43
|
+
@attributes[:access_token] || client.token
|
44
|
+
end
|
45
|
+
|
46
|
+
# Access secret for this request, either generated from the request token or taken
|
47
|
+
# from the :access_secret parameter.
|
48
|
+
#
|
49
|
+
def access_secret
|
50
|
+
@attributes[:access_secret] || client.secret
|
51
|
+
end
|
52
|
+
|
53
|
+
def client_from_request_data # :nodoc:
|
54
|
+
request_token = OAuth::RequestToken.new(consumer, @attributes[:request_token], @attributes[:request_secret])
|
55
|
+
request_token.get_access_token(:oauth_verifier => @attributes[:verifier])
|
56
|
+
end
|
57
|
+
|
58
|
+
def client_from_access_data # :nodoc:
|
59
|
+
OAuth::AccessToken.new(consumer, @attributes[:access_token], @attributes[:access_secret])
|
60
|
+
end
|
61
|
+
|
62
|
+
def client # :nodoc:
|
63
|
+
@client ||= has_access_data? ? client_from_access_data : client_from_request_data
|
64
|
+
end
|
65
|
+
|
66
|
+
# Fetch a raw response from the specified endpoint.
|
67
|
+
#
|
68
|
+
def get(endpoint)
|
69
|
+
client.get(endpoint)
|
70
|
+
end
|
71
|
+
|
72
|
+
def post(endpoint)
|
73
|
+
client.post(endpoint)
|
74
|
+
end
|
75
|
+
|
76
|
+
def put(endpoint)
|
77
|
+
client.put(endpoint)
|
78
|
+
end
|
79
|
+
|
80
|
+
def delete(endpoint)
|
81
|
+
client.delete(endpoint)
|
82
|
+
end
|
83
|
+
|
84
|
+
def post_multipart(endpoint, params = {})
|
85
|
+
Net::HTTP.new(Etsy.host).start do |http|
|
86
|
+
req = Net::HTTP::Post.new(endpoint)
|
87
|
+
add_multipart_data(req, params)
|
88
|
+
add_oauth(req)
|
89
|
+
res = http.request(req)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
# Encodes the request as multipart
|
96
|
+
def add_multipart_data(req, params)
|
97
|
+
crlf = "\r\n"
|
98
|
+
boundary = Time.now.to_i.to_s(16)
|
99
|
+
req["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
|
100
|
+
body = ""
|
101
|
+
params.each do |key,value|
|
102
|
+
esc_key = CGI.escape(key.to_s)
|
103
|
+
body << "--#{boundary}#{crlf}"
|
104
|
+
if value.respond_to?(:read)
|
105
|
+
body << "Content-Disposition: form-data; name=\"#{esc_key}\"; filename=\"#{File.basename(value.path)}\"#{crlf}"
|
106
|
+
body << "Content-Type: image/jpeg#{crlf*2}"
|
107
|
+
body << value.read
|
108
|
+
else
|
109
|
+
body << "Content-Disposition: form-data; name=\"#{esc_key}\"#{crlf*2}#{value}"
|
110
|
+
end
|
111
|
+
body << crlf
|
112
|
+
end
|
113
|
+
body << "--#{boundary}--#{crlf*2}"
|
114
|
+
req.body = body
|
115
|
+
req["Content-Length"] = req.body.size
|
116
|
+
end
|
117
|
+
|
118
|
+
# Uses the OAuth gem to add the signed Authorization header
|
119
|
+
def add_oauth(req)
|
120
|
+
client.sign!(req)
|
121
|
+
end
|
122
|
+
|
123
|
+
def has_access_data?
|
124
|
+
!@attributes[:access_token].nil? && !@attributes[:access_secret].nil?
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
end
|