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.
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