tailored-etsy 0.2.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 (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