etsy 0.2.0 → 0.2.1
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 +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
|