cardmagic-etsy 0.3.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.
Files changed (93) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.travis.yml +13 -0
  4. data/Gemfile +11 -0
  5. data/LICENSE +9 -0
  6. data/README.md +348 -0
  7. data/Rakefile +12 -0
  8. data/etsy.gemspec +34 -0
  9. data/lib/etsy.rb +223 -0
  10. data/lib/etsy/about.rb +15 -0
  11. data/lib/etsy/address.rb +47 -0
  12. data/lib/etsy/attribute_value.rb +46 -0
  13. data/lib/etsy/basic_client.rb +32 -0
  14. data/lib/etsy/category.rb +84 -0
  15. data/lib/etsy/country.rb +27 -0
  16. data/lib/etsy/favorite_listing.rb +26 -0
  17. data/lib/etsy/image.rb +44 -0
  18. data/lib/etsy/listing.rb +296 -0
  19. data/lib/etsy/model.rb +127 -0
  20. data/lib/etsy/payment_template.rb +33 -0
  21. data/lib/etsy/profile.rb +49 -0
  22. data/lib/etsy/receipt.rb +37 -0
  23. data/lib/etsy/request.rb +150 -0
  24. data/lib/etsy/response.rb +128 -0
  25. data/lib/etsy/section.rb +16 -0
  26. data/lib/etsy/secure_client.rb +131 -0
  27. data/lib/etsy/shipping_info.rb +27 -0
  28. data/lib/etsy/shipping_template.rb +41 -0
  29. data/lib/etsy/shop.rb +88 -0
  30. data/lib/etsy/transaction.rb +29 -0
  31. data/lib/etsy/user.rb +109 -0
  32. data/lib/etsy/variation/property_set.rb +71 -0
  33. data/lib/etsy/verification_request.rb +17 -0
  34. data/lib/etsy/version.rb +3 -0
  35. data/test/fixtures/about/getAbout.json +16 -0
  36. data/test/fixtures/address/getUserAddresses.json +12 -0
  37. data/test/fixtures/attribute_value/findAllListingPropertyValues.json +44 -0
  38. data/test/fixtures/category/findAllSubCategoryChildren.json +78 -0
  39. data/test/fixtures/category/findAllTopCategory.json +347 -0
  40. data/test/fixtures/category/findAllTopCategory.single.json +18 -0
  41. data/test/fixtures/category/findAllTopCategoryChildren.json +308 -0
  42. data/test/fixtures/category/getCategory.multiple.json +28 -0
  43. data/test/fixtures/category/getCategory.single.json +18 -0
  44. data/test/fixtures/country/getCountry.json +1 -0
  45. data/test/fixtures/favorite_listing/findAllFavoriteListings.json +1 -0
  46. data/test/fixtures/image/findAllListingImages.json +102 -0
  47. data/test/fixtures/listing/findAllListingActive.category.json +827 -0
  48. data/test/fixtures/listing/findAllShopListings.json +69 -0
  49. data/test/fixtures/listing/getListing.multiple.json +1 -0
  50. data/test/fixtures/listing/getListing.single.includeImages.json +1 -0
  51. data/test/fixtures/listing/getListing.single.json +1 -0
  52. data/test/fixtures/payment_template/getPaymentTemplate.json +1 -0
  53. data/test/fixtures/profile/new.json +28 -0
  54. data/test/fixtures/receipt/findAllShopReceipts.json +28 -0
  55. data/test/fixtures/section/getShopSection.json +18 -0
  56. data/test/fixtures/shipping_info/getShippingInfo.json +1 -0
  57. data/test/fixtures/shipping_template/getShippingTemplate.json +1 -0
  58. data/test/fixtures/shop/findAllShop.json +1 -0
  59. data/test/fixtures/shop/findAllShop.single.json +1 -0
  60. data/test/fixtures/shop/getShop.multiple.json +1 -0
  61. data/test/fixtures/shop/getShop.single.json +34 -0
  62. data/test/fixtures/transaction/findAllShopTransactions.json +1 -0
  63. data/test/fixtures/user/getUser.multiple.json +29 -0
  64. data/test/fixtures/user/getUser.single.json +13 -0
  65. data/test/fixtures/user/getUser.single.private.json +18 -0
  66. data/test/fixtures/user/getUser.single.withProfile.json +38 -0
  67. data/test/fixtures/user/getUser.single.withShops.json +41 -0
  68. data/test/test_helper.rb +45 -0
  69. data/test/unit/etsy/address_test.rb +61 -0
  70. data/test/unit/etsy/attribute_value_test.rb +67 -0
  71. data/test/unit/etsy/basic_client_test.rb +30 -0
  72. data/test/unit/etsy/category_test.rb +106 -0
  73. data/test/unit/etsy/country_test.rb +64 -0
  74. data/test/unit/etsy/favorite_listing_test.rb +44 -0
  75. data/test/unit/etsy/image_test.rb +51 -0
  76. data/test/unit/etsy/listing_test.rb +268 -0
  77. data/test/unit/etsy/model_test.rb +64 -0
  78. data/test/unit/etsy/payment_template_test.rb +68 -0
  79. data/test/unit/etsy/profile_test.rb +111 -0
  80. data/test/unit/etsy/receipt_test.rb +107 -0
  81. data/test/unit/etsy/request_test.rb +190 -0
  82. data/test/unit/etsy/response_test.rb +175 -0
  83. data/test/unit/etsy/section_test.rb +28 -0
  84. data/test/unit/etsy/secure_client_test.rb +132 -0
  85. data/test/unit/etsy/shipping_info_test.rb +24 -0
  86. data/test/unit/etsy/shipping_template_test.rb +24 -0
  87. data/test/unit/etsy/shop_about_test.rb +43 -0
  88. data/test/unit/etsy/shop_test.rb +116 -0
  89. data/test/unit/etsy/transaction_test.rb +61 -0
  90. data/test/unit/etsy/user_test.rb +250 -0
  91. data/test/unit/etsy/verification_request_test.rb +26 -0
  92. data/test/unit/etsy_test.rb +173 -0
  93. metadata +293 -0
@@ -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
@@ -0,0 +1,26 @@
1
+ module Etsy
2
+ class FavoriteListing
3
+ include Model
4
+
5
+ attributes :user_id, :listing_state, :listing_id, :create_date
6
+
7
+ #Create a new favorite listing
8
+ #
9
+ def self.create(user, listing, options = {})
10
+ options.merge!(:require_secure => true)
11
+ post("/users/#{user.id}/favorites/listings/#{listing.id}", options)
12
+ end
13
+
14
+ #Find all listings favorited by a user
15
+ #
16
+ def self.find_all_user_favorite_listings(user_id, options = {})
17
+ get_all("/users/#{user_id}/favorites/listings", options)
18
+ end
19
+
20
+ #Find a set of favorelistings associated with a listing_id
21
+ #
22
+ def self.find_all_listings_favored_by(listing_id, options = {})
23
+ get_all("/listings/#{listing_id}/favored-by", options)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,44 @@
1
+ module Etsy
2
+
3
+ # = Image
4
+ #
5
+ # Represents an image resource of an Etsy listing and contains multiple sizes.
6
+ # Sizes available are:
7
+ #
8
+ # [square] The square image thumbnail (75x75 pixels)
9
+ # [small] The small image thumbnail (170x135 pixels)
10
+ # [thumbnail] The thumbnail for the image, no more than 570px wide
11
+ # [full] The full image for this listing, no more than 1500px wide
12
+ #
13
+ class Image
14
+
15
+ include Etsy::Model
16
+
17
+ attribute :square, :from => :url_75x75
18
+ attribute :small, :from => :url_170x135
19
+ attribute :thumbnail, :from => :url_570xN
20
+ attribute :height, :from => :full_height
21
+ attribute :width, :from => :full_width
22
+ attribute :full, :from => :url_fullxfull
23
+
24
+ # Fetch all images for a given listing.
25
+ #
26
+ def self.find_all_by_listing_id(listing_id, options = {})
27
+ get_all("/listings/#{listing_id}/images", options)
28
+ end
29
+
30
+ def self.create(listing, image_path, options = {})
31
+ options.merge!(:require_secure => true)
32
+ options[:image] = File.new(image_path)
33
+ options[:multipart] = true
34
+ post("/listings/#{listing.id}/images", options)
35
+ end
36
+
37
+ # Delete image
38
+ #
39
+ def self.destroy(listing, image, options = {})
40
+ options.merge!(:require_secure => true)
41
+ delete("/listings/#{listing.id}/images/#{image.id}", options)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,296 @@
1
+ module Etsy
2
+
3
+ # = Listing
4
+ #
5
+ # Represents a single Etsy listing. Has the following attributes:
6
+ #
7
+ # [id] The unique identifier for this listing
8
+ # [title] The title of this listing
9
+ # [description] This listing's full description
10
+ # [view_count] The number of times this listing has been viewed
11
+ # [url] The full URL to this listing's detail page
12
+ # [price] The price of this listing item
13
+ # [currency] The currency that the seller is using for this listing item
14
+ # [quantity] The number of items available for sale
15
+ # [tags] An array of tags that the seller has used for this listing
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.
22
+ #
23
+ # Additionally, the following queries on this item are available:
24
+ #
25
+ # [active?] Is this listing active?
26
+ # [removed?] Has this listing been removed?
27
+ # [sold_out?] Is this listing sold out?
28
+ # [expired?] Has this listing expired?
29
+ # [alchemy?] Is this listing an Alchemy item? (i.e. requested by an Etsy user)
30
+ #
31
+ class Listing
32
+
33
+ include Etsy::Model
34
+
35
+ STATES = %w(active removed sold_out expired alchemy)
36
+ VALID_STATES = [:active, :expired, :inactive, :sold, :featured, :draft, :sold_out]
37
+
38
+ attribute :id, :from => :listing_id
39
+ attribute :view_count, :from => :views
40
+ attribute :created, :from => :creation_tsz
41
+ attribute :modified, :from => :last_modified_tsz
42
+ attribute :currency, :from => :currency_code
43
+ attribute :ending, :from => :ending_tsz
44
+ attribute :shop_section, :from => :shop_section_id
45
+
46
+ attributes :title, :description, :state, :url, :price, :quantity,
47
+ :tags, :materials, :hue, :saturation, :brightness, :is_black_and_white,
48
+ :featured_rank, :occasion, :num_favorers, :user_id,
49
+ :shipping_template_id, :who_made, :when_made,
50
+ :original_creation_tsz, :style, :category_path,
51
+ :taxonomy_id, :taxonomy_attributes
52
+
53
+ association :image, :from => 'Images'
54
+
55
+ def self.create(options = {})
56
+ options.merge!(:require_secure => true)
57
+ post("/listings", options)
58
+ end
59
+
60
+ def self.update(listing, options = {})
61
+ options.merge!(:require_secure => true)
62
+ put("/listings/#{listing.id}", options)
63
+ end
64
+
65
+ def self.destroy(listing, options = {})
66
+ options.merge!(:require_secure => true)
67
+ delete("/listings/#{listing.id}", options)
68
+ end
69
+
70
+ # Retrieve one or more listings by ID:
71
+ #
72
+ # Etsy::Listing.find(123)
73
+ #
74
+ # You can find multiple listings by passing an array of identifiers:
75
+ #
76
+ # Etsy::Listing.find([123, 456])
77
+ #
78
+ def self.find(*identifiers_and_options)
79
+ find_one_or_more('listings', identifiers_and_options)
80
+ end
81
+
82
+ # Retrieve listings for a given shop.
83
+ # By default, pulls back the first 25 active listings.
84
+ # Defaults can be overridden using :limit, :offset, and :state
85
+ #
86
+ # Available states are :active, :expired, :inactive, :sold, and :featured, :draft, :sold_out
87
+ # where :featured is a subset of the others.
88
+ #
89
+ # options = {
90
+ # :state => :expired,
91
+ # :limit => 100,
92
+ # :offset => 100,
93
+ # :token => 'toke',
94
+ # :secret => 'secret'
95
+ # }
96
+ # Etsy::Listing.find_all_by_shop_id(123, options)
97
+ #
98
+ def self.find_all_by_shop_id(shop_id, options = {})
99
+ state = options.delete(:state) || :active
100
+
101
+ raise(ArgumentError, self.invalid_state_message(state)) unless valid?(state)
102
+
103
+ if state == :sold
104
+ sold_listings(shop_id, options)
105
+ else
106
+ get_all("/shops/#{shop_id}/listings/#{state}", options)
107
+ end
108
+ end
109
+
110
+ # Retrieve active listings for a given category.
111
+ # By default, pulls back the first 25 active listings.
112
+ # Defaults can be overridden using :limit, :offset, and :state
113
+ #
114
+ # options = {
115
+ # :limit => 25,
116
+ # :offset => 100,
117
+ # :token => 'toke',
118
+ # :secret => 'secret'
119
+ # }
120
+ # Etsy::Listing.find_all_active_by_category("accessories", options)
121
+ #
122
+ def self.find_all_active_by_category(category, options = {})
123
+ options[:category] = category
124
+ get_all("/listings/active", options)
125
+ end
126
+
127
+ # The collection of images associated with this listing.
128
+ #
129
+ def images
130
+ @images ||= listing_images
131
+ end
132
+
133
+ # The primary image for this listing.
134
+ #
135
+ def image
136
+ images.first
137
+ end
138
+
139
+ # Listing category name
140
+ #
141
+ def category
142
+ path = category_path.join('/')
143
+ @category ||= Category.find(path)
144
+ end
145
+
146
+ # Returns the taxonomy defined attributes for the listing
147
+ #
148
+ def taxonomy_attributes(options={})
149
+ options.merge!(:require_secure => true)
150
+ self.class.get_all("/listings/#{id}/attributes", oauth.merge(options))
151
+ end
152
+
153
+ def variations(options={})
154
+ options.merge!(:require_secure => true)
155
+ self.class.get_all("/listings/#{id}/variations", oauth.merge(options))
156
+ end
157
+
158
+ # If these are your desired variations:
159
+ # - Dimensions: 1 x 2 inches
160
+ # - Dimensions: 2 x 4 inches
161
+ #
162
+ # Then you first have to find the property ID of the property you want to vary. Eg:
163
+ # Etsy::Variation::PropertySet.find_property_by_name("Dimensions").fetch("property_id")
164
+ #
165
+ # Then you can decide which options you want to set for this property.
166
+ # Eg:
167
+ # Etsy::Variation::PropertySet.qualifying_properties_for_property("Dimension")
168
+ # => [{
169
+ # "param"=>"dimensions_scale",
170
+ # "description"=>"Sizing Scale",
171
+ # "options"=>{
172
+ # "Inches" => 344,
173
+ # "Centimeters" => 345,
174
+ # "Other" => 346
175
+ # }
176
+ # }]
177
+ #
178
+ # Put it all together for a call to add_variations:
179
+
180
+ # property_id = Etsy::Variation::PropertySet.find_property_by_name("Dimensions").fetch("property_id")
181
+ # scale = Etsy::Variation::PropertySet.qualifying_properties_for_property("Dimensions").detect {|qp| qp.fetch("description") == "Sizing Scale"}
182
+ # my_listing.add_variations(
183
+ # :variations => [
184
+ # {"property_id" => property_id, "value" => "1 x 2", "is_available" => true, "price" => 1.23},
185
+ # {"property_id" => property_id, "value" => "2 x 4", "is_available" => true, "price" => 2.34}
186
+ # ],
187
+ # scale.fetch("param") => scale.fetch("options").fetch("Inches")
188
+ # )
189
+ def add_variations(options)
190
+ options[:variations] = JSON.dump(options.delete(:variations))
191
+ options[:require_secure] = true
192
+ self.class.post("/listings/#{id}/variations", options)
193
+ end
194
+
195
+ def update_variations(options)
196
+ options[:variations] = JSON.dump(options.delete(:variations))
197
+ options[:require_secure] = true
198
+ self.class.put("/listings/#{id}/variations", options)
199
+ end
200
+
201
+ def black_and_white?
202
+ is_black_and_white
203
+ end
204
+
205
+ STATES.each do |method_name|
206
+ define_method "#{method_name}?" do
207
+ state == method_name.sub('_', '')
208
+ end
209
+ end
210
+
211
+ # Time that this listing was created
212
+ #
213
+ def created_at
214
+ Time.at(created)
215
+ end
216
+
217
+ # Time that this listing was last modified
218
+ #
219
+ def modified_at
220
+ Time.at(modified)
221
+ end
222
+
223
+ # Time that this listing is ending (will be removed from store)
224
+ #
225
+ def ending_at
226
+ Time.at(ending)
227
+ end
228
+
229
+ #Return a list of users who have favorited this listing
230
+ #
231
+ def admirers(options = {})
232
+ options = options.merge(:access_token => token, :access_secret => secret) if (token && secret)
233
+ favorite_listings = FavoriteListing.find_all_listings_favored_by(id, options)
234
+ user_ids = favorite_listings.map {|f| f.user_id }.uniq
235
+ (user_ids.size > 0) ? Array(Etsy::User.find(user_ids, options)) : []
236
+ end
237
+
238
+ def is_supply
239
+ !!@result.fetch("is_supply")
240
+ end
241
+
242
+ private
243
+
244
+ def self.valid?(state)
245
+ VALID_STATES.include?(state)
246
+ end
247
+
248
+ def self.invalid_state_message(state)
249
+ "The state '#{state}' is invalid. Must be one of #{VALID_STATES.join(', ')}"
250
+ end
251
+
252
+ def self.sold_listings(shop_id, options = {})
253
+ includes = options.delete(:includes)
254
+
255
+ transactions = Transaction.find_all_by_shop_id(shop_id, options)
256
+ listing_ids = transactions.map {|t| t.listing_id }.uniq
257
+
258
+ options = options.merge(:includes => includes) if includes
259
+ (listing_ids.size > 0) ? Array(find(listing_ids, options)) : []
260
+ end
261
+
262
+ #Find all listings favored by a user
263
+ #
264
+ def self.find_all_user_favorite_listings(user_id, options = {})
265
+ favorite_listings = FavoriteListing.find_all_user_favorite_listings(user_id, options)
266
+ listing_ids = favorite_listings.map {|f| f.listing_id }.uniq
267
+ (listing_ids.size > 0) ? Array(find(listing_ids, options)) : []
268
+ end
269
+
270
+ #Find all listings that have been bought by a user
271
+ #
272
+ def self.bought_listings(user_id, options = {})
273
+ includes = options.delete(:includes)
274
+
275
+ transactions = Transaction.find_all_by_buyer_id(user_id, options)
276
+ listing_ids = transactions.map {|t| t.listing_id }.uniq
277
+
278
+ options = options.merge(:includes => includes) if includes
279
+ (listing_ids.size > 0) ? Array(find(listing_ids, options)) : []
280
+ end
281
+
282
+ private
283
+
284
+ def oauth
285
+ oauth = (token && secret) ? {:access_token => token, :access_secret => secret} : {}
286
+ end
287
+
288
+ def listing_images
289
+ if result && result["Images"]
290
+ result["Images"].map { |hash| Image.new(hash) }
291
+ else
292
+ Image.find_all_by_listing_id(id, oauth)
293
+ end
294
+ end
295
+ end
296
+ end
@@ -0,0 +1,127 @@
1
+ module Etsy
2
+ module Model # :nodoc:all
3
+
4
+ module ClassMethods
5
+
6
+ def attribute(name, options = {})
7
+ define_method name do
8
+ @result[options.fetch(:from, name).to_s]
9
+ end
10
+ end
11
+
12
+ def attributes(*names)
13
+ names.each {|name| attribute(name) }
14
+ end
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.total - 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 delete(endpoint, options={})
88
+ Request.delete(endpoint, options)
89
+ end
90
+
91
+ def find_one_or_more(endpoint, identifiers_and_options)
92
+ options = options_from(identifiers_and_options)
93
+ append = options.delete(:append_to_endpoint)
94
+ append = append.nil? ? "" : "/#{append}"
95
+ identifiers = identifiers_and_options
96
+ get("/#{endpoint}/#{identifiers.join(',')}#{append}", options)
97
+ end
98
+
99
+ def options_from(argument)
100
+ (argument.last.class == Hash) ? argument.pop : {}
101
+ end
102
+
103
+ end
104
+
105
+ def initialize(result = nil, token = nil, secret = nil)
106
+ @result = result
107
+ @token = token
108
+ @secret = secret
109
+ end
110
+
111
+ def token
112
+ @token
113
+ end
114
+
115
+ def secret
116
+ @secret
117
+ end
118
+
119
+ def result
120
+ @result
121
+ end
122
+
123
+ def self.included(other)
124
+ other.extend ClassMethods
125
+ end
126
+ end
127
+ end