etsy 0.2.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +8 -0
- data/.travis.yml +8 -0
- data/Gemfile +10 -0
- data/README.md +300 -0
- data/Rakefile +2 -30
- data/etsy.gemspec +36 -0
- data/lib/etsy.rb +46 -17
- data/lib/etsy/address.rb +47 -0
- data/lib/etsy/basic_client.rb +1 -1
- data/lib/etsy/category.rb +84 -0
- data/lib/etsy/country.rb +27 -0
- data/lib/etsy/image.rb +7 -3
- data/lib/etsy/listing.rb +107 -8
- data/lib/etsy/model.rb +99 -3
- data/lib/etsy/payment_template.rb +33 -0
- data/lib/etsy/profile.rb +49 -0
- data/lib/etsy/request.rb +85 -17
- data/lib/etsy/response.rb +80 -4
- data/lib/etsy/section.rb +16 -0
- data/lib/etsy/secure_client.rb +49 -4
- data/lib/etsy/shipping_template.rb +32 -0
- data/lib/etsy/shop.rb +21 -12
- data/lib/etsy/transaction.rb +18 -0
- data/lib/etsy/user.rb +45 -13
- data/lib/etsy/verification_request.rb +2 -2
- 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/listing/findAllListingActive.category.json +827 -0
- data/test/fixtures/listing/{findAllShopListingsActive.json → findAllShopListings.json} +0 -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/getShop.single.json +4 -3
- data/test/fixtures/transaction/findAllShopTransactions.json +1 -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 +9 -4
- data/test/unit/etsy/address_test.rb +61 -0
- data/test/unit/etsy/category_test.rb +106 -0
- data/test/unit/etsy/country_test.rb +64 -0
- data/test/unit/etsy/listing_test.rb +112 -5
- 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 +89 -53
- data/test/unit/etsy/response_test.rb +118 -4
- data/test/unit/etsy/section_test.rb +28 -0
- data/test/unit/etsy/secure_client_test.rb +27 -5
- data/test/unit/etsy/shipping_template_test.rb +24 -0
- data/test/unit/etsy/shop_test.rb +12 -5
- data/test/unit/etsy/transaction_test.rb +52 -0
- data/test/unit/etsy/user_test.rb +147 -8
- data/test/unit/etsy/verification_request_test.rb +3 -3
- data/test/unit/etsy_test.rb +19 -7
- metadata +133 -77
- data/README.rdoc +0 -208
- data/lib/etsy/version.rb +0 -13
data/lib/etsy/address.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
module Etsy
|
2
|
+
|
3
|
+
# = Address
|
4
|
+
#
|
5
|
+
# Represents a single Etsy Address. Users may or may not have associated addresses.
|
6
|
+
#
|
7
|
+
# An address has the following attributes:
|
8
|
+
#
|
9
|
+
# [first_line] Street address
|
10
|
+
# [second_line] Additional street information.
|
11
|
+
# [city]
|
12
|
+
# [state]
|
13
|
+
# [country]
|
14
|
+
# [country_id] The Etsy country id
|
15
|
+
#
|
16
|
+
class Address
|
17
|
+
|
18
|
+
include Etsy::Model
|
19
|
+
|
20
|
+
attributes :name, :first_line, :second_line, :city, :state, :zip, :country_id
|
21
|
+
|
22
|
+
attribute :id, :from => :user_address_id
|
23
|
+
attribute :country, :from => :country_name
|
24
|
+
|
25
|
+
# Retrieve all of a user's addresses by user name or ID:
|
26
|
+
#
|
27
|
+
# Etsy::Address.find('reagent')
|
28
|
+
#
|
29
|
+
def self.find(*identifiers_and_options)
|
30
|
+
self.append_to_endpoint('addresses', identifiers_and_options)
|
31
|
+
self.find_one_or_more('users', identifiers_and_options)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
def oauth
|
36
|
+
oauth = (token && secret) ? {:access_token => token, :access_secret => secret} : {}
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.append_to_endpoint(suffix, arguments)
|
40
|
+
if arguments.last.class == Hash
|
41
|
+
arguments.last[:append_to_endpoint] = suffix
|
42
|
+
else
|
43
|
+
arguments << {:append_to_endpoint => suffix}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/etsy/basic_client.rb
CHANGED
@@ -0,0 +1,84 @@
|
|
1
|
+
module Etsy
|
2
|
+
|
3
|
+
# = Category
|
4
|
+
#
|
5
|
+
# A category of listings for sale, formed from 1 to 3 tags.
|
6
|
+
#
|
7
|
+
# A category has the following attributes:
|
8
|
+
#
|
9
|
+
# [page_description] A short description of the category from the category's landing page
|
10
|
+
# [page_title] The title of the category's landing page
|
11
|
+
# [category_name] The category's string ID
|
12
|
+
# [short_name] A short display name for the category
|
13
|
+
# [long_name] A longer display name for the category
|
14
|
+
# [children_count] The number of subcategories below this one
|
15
|
+
#
|
16
|
+
class Category
|
17
|
+
|
18
|
+
include Etsy::Model
|
19
|
+
|
20
|
+
attribute :id, :from => :category_id
|
21
|
+
attribute :children_count, :from => :num_children
|
22
|
+
attributes :page_description, :page_title, :category_name, :short_name,
|
23
|
+
:long_name
|
24
|
+
|
25
|
+
# Retrieve one or more top-level categories by name:
|
26
|
+
#
|
27
|
+
# Etsy::Category.find('accessories')
|
28
|
+
#
|
29
|
+
# You can find multiple top-level categories by passing an array of identifiers:
|
30
|
+
#
|
31
|
+
# Etsy::Category.find(['accessories', 'art'])
|
32
|
+
#
|
33
|
+
def self.find_top(*identifiers_and_options)
|
34
|
+
self.find_one_or_more('categories', identifiers_and_options)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Retrieve a list of all subcategories of the specified category.
|
38
|
+
#
|
39
|
+
# Etsy::Category.find_all_subcategories('accessories')
|
40
|
+
#
|
41
|
+
# You can also find the subcategories of a subcategory.
|
42
|
+
#
|
43
|
+
# Etsy::Category.find_all_subcategories('accessories/apron')
|
44
|
+
#
|
45
|
+
# Etsy categories only go three levels deep, so this will return nil past the third level.
|
46
|
+
#
|
47
|
+
# Etsy::Category.find_all_subcategories('accessories/apron/women')
|
48
|
+
# => nil
|
49
|
+
#
|
50
|
+
def self.find_all_subcategories(category, options = {})
|
51
|
+
valid?(category) ? self.get_all("/taxonomy/categories/#{category}", options) : nil
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.find(tag)
|
55
|
+
get("/categories/#{tag}")
|
56
|
+
end
|
57
|
+
|
58
|
+
# Retrieve a list of all top-level categories.
|
59
|
+
#
|
60
|
+
# Etsy::Category.all
|
61
|
+
#
|
62
|
+
def self.all_top(options = {})
|
63
|
+
self.get_all("/taxonomy/categories", options)
|
64
|
+
end
|
65
|
+
|
66
|
+
# The collection of active listings associated with this category.
|
67
|
+
#
|
68
|
+
def active_listings
|
69
|
+
@active_listings ||= Listing.find_all_active_by_category(category_name)
|
70
|
+
end
|
71
|
+
|
72
|
+
# The collection of subcategories associated with this category.
|
73
|
+
#
|
74
|
+
def subcategories
|
75
|
+
@subcategories ||= Category.find_all_subcategories(category_name)
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def self.valid?(parent_category)
|
81
|
+
parent_category.count("/") < 2
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
data/lib/etsy/country.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
module Etsy
|
2
|
+
class Country
|
3
|
+
include Etsy::Model
|
4
|
+
|
5
|
+
attribute :id, :from => :country_id
|
6
|
+
attributes :iso_country_code, :world_bank_country_code
|
7
|
+
attributes :name, :slug, :lat, :lon
|
8
|
+
|
9
|
+
def self.find_all
|
10
|
+
get("/countries")
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.find(id)
|
14
|
+
get("/countries/#{id}")
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.find_by_alpha2(alpha2)
|
18
|
+
alpha2 = alpha2.upcase
|
19
|
+
find_all.detect { |country| country.iso_country_code == alpha2}
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.find_by_world_bank_country_code(world_bank_country_code)
|
23
|
+
world_bank_country_code = world_bank_country_code.upcase
|
24
|
+
find_all.detect { |country| country.world_bank_country_code == world_bank_country_code}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/etsy/image.rb
CHANGED
@@ -22,9 +22,13 @@ module Etsy
|
|
22
22
|
# Fetch all images for a given listing.
|
23
23
|
#
|
24
24
|
def self.find_all_by_listing_id(listing_id)
|
25
|
-
|
26
|
-
[response.result].flatten.map {|data| new(data) }
|
25
|
+
get_all("/listings/#{listing_id}/images")
|
27
26
|
end
|
28
27
|
|
28
|
+
def self.create(listing, image_path, options = {})
|
29
|
+
options[:image] = File.new(image_path)
|
30
|
+
options[:multipart] = true
|
31
|
+
post("/listings/#{listing.id}/images", options)
|
32
|
+
end
|
29
33
|
end
|
30
|
-
end
|
34
|
+
end
|
data/lib/etsy/listing.rb
CHANGED
@@ -14,6 +14,11 @@ module Etsy
|
|
14
14
|
# [quantity] The number of items available for sale
|
15
15
|
# [tags] An array of tags that the seller has used for this listing
|
16
16
|
# [materials] Any array of materials that was used in the production of this item
|
17
|
+
# [state] The current state of the item
|
18
|
+
# [hue] The hue of the listing's primary image (HSV color).
|
19
|
+
# [saturation] The saturation of the listing's primary image (HSV color).
|
20
|
+
# [brightness] The value of the listing's primary image (HSV color).
|
21
|
+
# [black_and_white?] True if the listing's primary image is in black & white.
|
17
22
|
#
|
18
23
|
# Additionally, the following queries on this item are available:
|
19
24
|
#
|
@@ -28,21 +33,85 @@ module Etsy
|
|
28
33
|
include Etsy::Model
|
29
34
|
|
30
35
|
STATES = %w(active removed sold_out expired alchemy)
|
36
|
+
VALID_STATES = [:active, :expired, :inactive, :sold, :featured]
|
31
37
|
|
32
38
|
attribute :id, :from => :listing_id
|
33
39
|
attribute :view_count, :from => :views
|
34
40
|
attribute :created, :from => :creation_tsz
|
41
|
+
attribute :modified, :from => :last_modified_tsz
|
35
42
|
attribute :currency, :from => :currency_code
|
36
43
|
attribute :ending, :from => :ending_tsz
|
37
44
|
|
38
45
|
attributes :title, :description, :state, :url, :price, :quantity,
|
39
|
-
:tags, :materials
|
46
|
+
:tags, :materials, :hue, :saturation, :brightness, :is_black_and_white
|
40
47
|
|
41
|
-
|
48
|
+
association :image, :from => 'Images'
|
49
|
+
|
50
|
+
def self.create(options = {})
|
51
|
+
options.merge!(:require_secure => true)
|
52
|
+
post("/listings", options)
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.update(listing, options = {})
|
56
|
+
options.merge!(:require_secure => true)
|
57
|
+
put("/listings/#{listing.id}", options)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Retrieve one or more listings by ID:
|
61
|
+
#
|
62
|
+
# Etsy::Listing.find(123)
|
63
|
+
#
|
64
|
+
# You can find multiple listings by passing an array of identifiers:
|
65
|
+
#
|
66
|
+
# Etsy::Listing.find([123, 456])
|
67
|
+
#
|
68
|
+
def self.find(*identifiers_and_options)
|
69
|
+
find_one_or_more('listings', identifiers_and_options)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Retrieve listings for a given shop.
|
73
|
+
# By default, pulls back the first 25 active listings.
|
74
|
+
# Defaults can be overridden using :limit, :offset, and :state
|
75
|
+
#
|
76
|
+
# Available states are :active, :expired, :inactive, :sold, and :featured
|
77
|
+
# where :featured is a subset of the others.
|
78
|
+
#
|
79
|
+
# options = {
|
80
|
+
# :state => :expired,
|
81
|
+
# :limit => 100,
|
82
|
+
# :offset => 100,
|
83
|
+
# :token => 'toke',
|
84
|
+
# :secret => 'secret'
|
85
|
+
# }
|
86
|
+
# Etsy::Listing.find_all_by_shop_id(123, options)
|
87
|
+
#
|
88
|
+
def self.find_all_by_shop_id(shop_id, options = {})
|
89
|
+
state = options.delete(:state) || :active
|
90
|
+
|
91
|
+
raise(ArgumentError, self.invalid_state_message(state)) unless valid?(state)
|
92
|
+
|
93
|
+
if state == :sold
|
94
|
+
sold_listings(shop_id, options)
|
95
|
+
else
|
96
|
+
get_all("/shops/#{shop_id}/listings/#{state}", options)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Retrieve active listings for a given category.
|
101
|
+
# By default, pulls back the first 25 active listings.
|
102
|
+
# Defaults can be overridden using :limit, :offset, and :state
|
103
|
+
#
|
104
|
+
# options = {
|
105
|
+
# :limit => 25,
|
106
|
+
# :offset => 100,
|
107
|
+
# :token => 'toke',
|
108
|
+
# :secret => 'secret'
|
109
|
+
# }
|
110
|
+
# Etsy::Listing.find_all_active_by_category("accessories", options)
|
42
111
|
#
|
43
|
-
def self.
|
44
|
-
|
45
|
-
|
112
|
+
def self.find_all_active_by_category(category, options = {})
|
113
|
+
options[:category] = category
|
114
|
+
get_all("/listings/active", options)
|
46
115
|
end
|
47
116
|
|
48
117
|
# The collection of images associated with this listing.
|
@@ -57,9 +126,13 @@ module Etsy
|
|
57
126
|
images.first
|
58
127
|
end
|
59
128
|
|
60
|
-
|
61
|
-
|
62
|
-
|
129
|
+
def black_and_white?
|
130
|
+
is_black_and_white
|
131
|
+
end
|
132
|
+
|
133
|
+
STATES.each do |method_name|
|
134
|
+
define_method "#{method_name}?" do
|
135
|
+
state == method_name.sub('_', '')
|
63
136
|
end
|
64
137
|
end
|
65
138
|
|
@@ -69,11 +142,37 @@ module Etsy
|
|
69
142
|
Time.at(created)
|
70
143
|
end
|
71
144
|
|
145
|
+
# Time that this listing was last modified
|
146
|
+
#
|
147
|
+
def modified_at
|
148
|
+
Time.at(modified)
|
149
|
+
end
|
150
|
+
|
72
151
|
# Time that this listing is ending (will be removed from store)
|
73
152
|
#
|
74
153
|
def ending_at
|
75
154
|
Time.at(ending)
|
76
155
|
end
|
77
156
|
|
157
|
+
private
|
158
|
+
|
159
|
+
def self.valid?(state)
|
160
|
+
VALID_STATES.include?(state)
|
161
|
+
end
|
162
|
+
|
163
|
+
def self.invalid_state_message(state)
|
164
|
+
"The state '#{state}' is invalid. Must be one of #{VALID_STATES.join(', ')}"
|
165
|
+
end
|
166
|
+
|
167
|
+
def self.sold_listings(shop_id, options = {})
|
168
|
+
includes = options.delete(:includes)
|
169
|
+
|
170
|
+
transactions = Transaction.find_all_by_shop_id(shop_id, options)
|
171
|
+
listing_ids = transactions.map {|t| t.listing_id }.uniq
|
172
|
+
|
173
|
+
options = options.merge(:includes => includes) if includes
|
174
|
+
(listing_ids.size > 0) ? Array(find(listing_ids, options)) : []
|
175
|
+
end
|
176
|
+
|
78
177
|
end
|
79
178
|
end
|
data/lib/etsy/model.rb
CHANGED
@@ -13,15 +13,111 @@ module Etsy
|
|
13
13
|
names.each {|name| attribute(name) }
|
14
14
|
end
|
15
15
|
|
16
|
+
# FIXME: not quite sure where I'm going with this yet. KO.
|
17
|
+
def association(name, options = {})
|
18
|
+
define_method "associated_#{name}" do
|
19
|
+
@result[options.fetch(:from, name).to_s]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def get(endpoint, options = {})
|
24
|
+
objects = get_all(endpoint, options)
|
25
|
+
if objects.length == 0
|
26
|
+
nil
|
27
|
+
elsif objects.length == 1
|
28
|
+
objects[0]
|
29
|
+
else
|
30
|
+
objects
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def get_all(endpoint, options={})
|
35
|
+
limit = options[:limit]
|
36
|
+
|
37
|
+
if limit
|
38
|
+
initial_offset = options.fetch(:offset, 0)
|
39
|
+
batch_size = options.fetch(:batch_size, 100)
|
40
|
+
|
41
|
+
result = []
|
42
|
+
|
43
|
+
if limit == :all
|
44
|
+
response = Request.get(endpoint, options.merge(:limit => batch_size, :offset => initial_offset))
|
45
|
+
result << response.result
|
46
|
+
limit = [response.count - batch_size - initial_offset, 0].max
|
47
|
+
initial_offset += batch_size
|
48
|
+
end
|
49
|
+
|
50
|
+
num_batches = limit / batch_size
|
51
|
+
|
52
|
+
num_batches.times do |batch|
|
53
|
+
total_offset = initial_offset + batch * batch_size
|
54
|
+
response = Request.get(endpoint, options.merge(:limit => batch_size, :offset => total_offset))
|
55
|
+
result << response.result
|
56
|
+
end
|
57
|
+
|
58
|
+
remainder = limit % batch_size
|
59
|
+
|
60
|
+
if remainder > 0
|
61
|
+
total_offset = initial_offset + num_batches * batch_size
|
62
|
+
response = Request.get(endpoint, options.merge(:limit => remainder, :offset => total_offset))
|
63
|
+
result << response.result
|
64
|
+
end
|
65
|
+
else
|
66
|
+
response = Request.get(endpoint, options)
|
67
|
+
result = response.result
|
68
|
+
end
|
69
|
+
|
70
|
+
[result].flatten.map do |data|
|
71
|
+
if options[:access_token] && options[:access_secret]
|
72
|
+
new(data, options[:access_token], options[:access_secret])
|
73
|
+
else
|
74
|
+
new(data)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def post(endpoint, options={})
|
80
|
+
Request.post(endpoint, options)
|
81
|
+
end
|
82
|
+
|
83
|
+
def put(endpoint, options={})
|
84
|
+
Request.put(endpoint, options)
|
85
|
+
end
|
86
|
+
|
87
|
+
def find_one_or_more(endpoint, identifiers_and_options)
|
88
|
+
options = options_from(identifiers_and_options)
|
89
|
+
append = options.delete(:append_to_endpoint)
|
90
|
+
append = append.nil? ? "" : "/#{append}"
|
91
|
+
identifiers = identifiers_and_options
|
92
|
+
get("/#{endpoint}/#{identifiers.join(',')}#{append}", options)
|
93
|
+
end
|
94
|
+
|
95
|
+
def options_from(argument)
|
96
|
+
(argument.last.class == Hash) ? argument.pop : {}
|
97
|
+
end
|
98
|
+
|
16
99
|
end
|
17
100
|
|
18
|
-
def initialize(result = nil)
|
101
|
+
def initialize(result = nil, token = nil, secret = nil)
|
19
102
|
@result = result
|
103
|
+
@token = token
|
104
|
+
@secret = secret
|
105
|
+
end
|
106
|
+
|
107
|
+
def token
|
108
|
+
@token
|
109
|
+
end
|
110
|
+
|
111
|
+
def secret
|
112
|
+
@secret
|
113
|
+
end
|
114
|
+
|
115
|
+
def result
|
116
|
+
@result
|
20
117
|
end
|
21
118
|
|
22
119
|
def self.included(other)
|
23
120
|
other.extend ClassMethods
|
24
121
|
end
|
25
|
-
|
26
122
|
end
|
27
|
-
end
|
123
|
+
end
|