tailored-etsy 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. data/.gitignore +8 -0
  2. data/.travis.yml +8 -0
  3. data/Gemfile +3 -0
  4. data/LICENSE +9 -0
  5. data/README.md +280 -0
  6. data/Rakefile +12 -0
  7. data/etsy.gemspec +28 -0
  8. data/lib/etsy.rb +172 -0
  9. data/lib/etsy/address.rb +47 -0
  10. data/lib/etsy/basic_client.rb +26 -0
  11. data/lib/etsy/category.rb +84 -0
  12. data/lib/etsy/country.rb +27 -0
  13. data/lib/etsy/image.rb +34 -0
  14. data/lib/etsy/listing.rb +178 -0
  15. data/lib/etsy/model.rb +123 -0
  16. data/lib/etsy/payment_template.rb +33 -0
  17. data/lib/etsy/profile.rb +49 -0
  18. data/lib/etsy/request.rb +148 -0
  19. data/lib/etsy/response.rb +112 -0
  20. data/lib/etsy/section.rb +16 -0
  21. data/lib/etsy/secure_client.rb +128 -0
  22. data/lib/etsy/shipping_template.rb +32 -0
  23. data/lib/etsy/shop.rb +83 -0
  24. data/lib/etsy/transaction.rb +18 -0
  25. data/lib/etsy/user.rb +91 -0
  26. data/lib/etsy/verification_request.rb +17 -0
  27. data/lib/etsy/version.rb +3 -0
  28. data/test/fixtures/address/getUserAddresses.json +12 -0
  29. data/test/fixtures/category/findAllSubCategoryChildren.json +78 -0
  30. data/test/fixtures/category/findAllTopCategory.json +347 -0
  31. data/test/fixtures/category/findAllTopCategory.single.json +18 -0
  32. data/test/fixtures/category/findAllTopCategoryChildren.json +308 -0
  33. data/test/fixtures/category/getCategory.multiple.json +28 -0
  34. data/test/fixtures/category/getCategory.single.json +18 -0
  35. data/test/fixtures/country/getCountry.json +1 -0
  36. data/test/fixtures/image/findAllListingImages.json +102 -0
  37. data/test/fixtures/listing/findAllListingActive.category.json +827 -0
  38. data/test/fixtures/listing/findAllShopListings.json +69 -0
  39. data/test/fixtures/listing/getListing.multiple.json +1 -0
  40. data/test/fixtures/listing/getListing.single.json +1 -0
  41. data/test/fixtures/payment_template/getPaymentTemplate.json +1 -0
  42. data/test/fixtures/profile/new.json +28 -0
  43. data/test/fixtures/section/getShopSection.json +18 -0
  44. data/test/fixtures/shipping_template/getShippingTemplate.json +1 -0
  45. data/test/fixtures/shop/findAllShop.json +1 -0
  46. data/test/fixtures/shop/findAllShop.single.json +1 -0
  47. data/test/fixtures/shop/getShop.multiple.json +1 -0
  48. data/test/fixtures/shop/getShop.single.json +33 -0
  49. data/test/fixtures/transaction/findAllShopTransactions.json +1 -0
  50. data/test/fixtures/user/getUser.multiple.json +29 -0
  51. data/test/fixtures/user/getUser.single.json +13 -0
  52. data/test/fixtures/user/getUser.single.private.json +18 -0
  53. data/test/fixtures/user/getUser.single.withProfile.json +38 -0
  54. data/test/fixtures/user/getUser.single.withShops.json +41 -0
  55. data/test/test_helper.rb +44 -0
  56. data/test/unit/etsy/address_test.rb +61 -0
  57. data/test/unit/etsy/basic_client_test.rb +28 -0
  58. data/test/unit/etsy/category_test.rb +106 -0
  59. data/test/unit/etsy/country_test.rb +64 -0
  60. data/test/unit/etsy/image_test.rb +43 -0
  61. data/test/unit/etsy/listing_test.rb +217 -0
  62. data/test/unit/etsy/model_test.rb +64 -0
  63. data/test/unit/etsy/payment_template_test.rb +68 -0
  64. data/test/unit/etsy/profile_test.rb +111 -0
  65. data/test/unit/etsy/request_test.rb +192 -0
  66. data/test/unit/etsy/response_test.rb +164 -0
  67. data/test/unit/etsy/section_test.rb +28 -0
  68. data/test/unit/etsy/secure_client_test.rb +132 -0
  69. data/test/unit/etsy/shipping_template_test.rb +24 -0
  70. data/test/unit/etsy/shop_test.rb +104 -0
  71. data/test/unit/etsy/transaction_test.rb +52 -0
  72. data/test/unit/etsy/user_test.rb +218 -0
  73. data/test/unit/etsy/verification_request_test.rb +26 -0
  74. data/test/unit/etsy_test.rb +114 -0
  75. metadata +269 -0
@@ -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
@@ -0,0 +1,26 @@
1
+ module Etsy
2
+
3
+ # = BasicClient
4
+ #
5
+ # Used for calling public API methods.
6
+ #
7
+ class BasicClient
8
+
9
+ # Create a new client that will connect to the specified host
10
+ #
11
+ def initialize(host)
12
+ @host = host
13
+ end
14
+
15
+ def client # :nodoc:
16
+ @client ||= Net::HTTP.new(@host)
17
+ end
18
+
19
+ # Fetch a raw response from the specified endpoint
20
+ #
21
+ def get(endpoint)
22
+ client.get(endpoint)
23
+ end
24
+
25
+ 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
@@ -0,0 +1,34 @@
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 :full, :from => :url_fullxfull
21
+
22
+ # Fetch all images for a given listing.
23
+ #
24
+ def self.find_all_by_listing_id(listing_id)
25
+ get_all("/listings/#{listing_id}/images")
26
+ end
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
33
+ end
34
+ end
@@ -0,0 +1,178 @@
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]
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
+
45
+ attributes :title, :description, :state, :url, :price, :quantity,
46
+ :tags, :materials, :hue, :saturation, :brightness, :is_black_and_white
47
+
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)
111
+ #
112
+ def self.find_all_active_by_category(category, options = {})
113
+ options[:category] = category
114
+ get_all("/listings/active", options)
115
+ end
116
+
117
+ # The collection of images associated with this listing.
118
+ #
119
+ def images
120
+ @images ||= Image.find_all_by_listing_id(id)
121
+ end
122
+
123
+ # The primary image for this listing.
124
+ #
125
+ def image
126
+ images.first
127
+ end
128
+
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('_', '')
136
+ end
137
+ end
138
+
139
+ # Time that this listing was created
140
+ #
141
+ def created_at
142
+ Time.at(created)
143
+ end
144
+
145
+ # Time that this listing was last modified
146
+ #
147
+ def modified_at
148
+ Time.at(modified)
149
+ end
150
+
151
+ # Time that this listing is ending (will be removed from store)
152
+ #
153
+ def ending_at
154
+ Time.at(ending)
155
+ end
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
+
177
+ end
178
+ end
@@ -0,0 +1,123 @@
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.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
+
99
+ end
100
+
101
+ def initialize(result = nil, token = nil, secret = nil)
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
117
+ end
118
+
119
+ def self.included(other)
120
+ other.extend ClassMethods
121
+ end
122
+ end
123
+ end