etsy 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. data/.gitignore +8 -0
  2. data/.travis.yml +8 -0
  3. data/Gemfile +10 -0
  4. data/README.md +300 -0
  5. data/Rakefile +2 -30
  6. data/etsy.gemspec +36 -0
  7. data/lib/etsy.rb +46 -17
  8. data/lib/etsy/address.rb +47 -0
  9. data/lib/etsy/basic_client.rb +1 -1
  10. data/lib/etsy/category.rb +84 -0
  11. data/lib/etsy/country.rb +27 -0
  12. data/lib/etsy/image.rb +7 -3
  13. data/lib/etsy/listing.rb +107 -8
  14. data/lib/etsy/model.rb +99 -3
  15. data/lib/etsy/payment_template.rb +33 -0
  16. data/lib/etsy/profile.rb +49 -0
  17. data/lib/etsy/request.rb +85 -17
  18. data/lib/etsy/response.rb +80 -4
  19. data/lib/etsy/section.rb +16 -0
  20. data/lib/etsy/secure_client.rb +49 -4
  21. data/lib/etsy/shipping_template.rb +32 -0
  22. data/lib/etsy/shop.rb +21 -12
  23. data/lib/etsy/transaction.rb +18 -0
  24. data/lib/etsy/user.rb +45 -13
  25. data/lib/etsy/verification_request.rb +2 -2
  26. data/test/fixtures/address/getUserAddresses.json +12 -0
  27. data/test/fixtures/category/findAllSubCategoryChildren.json +78 -0
  28. data/test/fixtures/category/findAllTopCategory.json +347 -0
  29. data/test/fixtures/category/findAllTopCategory.single.json +18 -0
  30. data/test/fixtures/category/findAllTopCategoryChildren.json +308 -0
  31. data/test/fixtures/category/getCategory.multiple.json +28 -0
  32. data/test/fixtures/category/getCategory.single.json +18 -0
  33. data/test/fixtures/country/getCountry.json +1 -0
  34. data/test/fixtures/listing/findAllListingActive.category.json +827 -0
  35. data/test/fixtures/listing/{findAllShopListingsActive.json → findAllShopListings.json} +0 -0
  36. data/test/fixtures/listing/getListing.multiple.json +1 -0
  37. data/test/fixtures/listing/getListing.single.json +1 -0
  38. data/test/fixtures/payment_template/getPaymentTemplate.json +1 -0
  39. data/test/fixtures/profile/new.json +28 -0
  40. data/test/fixtures/section/getShopSection.json +18 -0
  41. data/test/fixtures/shipping_template/getShippingTemplate.json +1 -0
  42. data/test/fixtures/shop/getShop.single.json +4 -3
  43. data/test/fixtures/transaction/findAllShopTransactions.json +1 -0
  44. data/test/fixtures/user/getUser.single.withProfile.json +38 -0
  45. data/test/fixtures/user/getUser.single.withShops.json +41 -0
  46. data/test/test_helper.rb +9 -4
  47. data/test/unit/etsy/address_test.rb +61 -0
  48. data/test/unit/etsy/category_test.rb +106 -0
  49. data/test/unit/etsy/country_test.rb +64 -0
  50. data/test/unit/etsy/listing_test.rb +112 -5
  51. data/test/unit/etsy/model_test.rb +64 -0
  52. data/test/unit/etsy/payment_template_test.rb +68 -0
  53. data/test/unit/etsy/profile_test.rb +111 -0
  54. data/test/unit/etsy/request_test.rb +89 -53
  55. data/test/unit/etsy/response_test.rb +118 -4
  56. data/test/unit/etsy/section_test.rb +28 -0
  57. data/test/unit/etsy/secure_client_test.rb +27 -5
  58. data/test/unit/etsy/shipping_template_test.rb +24 -0
  59. data/test/unit/etsy/shop_test.rb +12 -5
  60. data/test/unit/etsy/transaction_test.rb +52 -0
  61. data/test/unit/etsy/user_test.rb +147 -8
  62. data/test/unit/etsy/verification_request_test.rb +3 -3
  63. data/test/unit/etsy_test.rb +19 -7
  64. metadata +133 -77
  65. data/README.rdoc +0 -208
  66. data/lib/etsy/version.rb +0 -13
@@ -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
@@ -23,4 +23,4 @@ module Etsy
23
23
  end
24
24
 
25
25
  end
26
- end
26
+ end
@@ -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
@@ -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
- response = Request.get("/listings/#{listing_id}/images")
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
- # Retrieve all active listings for a given shop. Pulls back the first 25 listings.
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.find_all_by_shop_id(shop_id)
44
- response = Request.get("/shops/#{shop_id}/listings/active")
45
- [response.result].flatten.map {|data| new(data) }
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
- STATES.each do |state|
61
- define_method "#{state}?" do
62
- self.state == state.sub('_', '')
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