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,33 @@
1
+ module Etsy
2
+ class PaymentTemplate
3
+ include Etsy::Model
4
+
5
+ attribute :id, :from => :payment_template_id
6
+ attributes :allow_check, :allow_mo, :allow_other, :allow_paypal, :allow_cc
7
+ attributes :paypal_email, :name, :first_line, :second_line, :city, :state
8
+ attributes :zip, :country_id
9
+
10
+ def self.create(options = {})
11
+ options.merge!(:require_secure => true)
12
+ post("/payments/templates", options)
13
+ end
14
+
15
+ def self.find(id, credentials = {})
16
+ options = {
17
+ :access_token => credentials[:access_token],
18
+ :access_secret => credentials[:access_secret],
19
+ :require_secure => true
20
+ }
21
+ get("/payments/templates/#{id}", options)
22
+ end
23
+
24
+ def self.find_by_user(user, credentials = {})
25
+ options = {
26
+ :access_token => credentials[:access_token],
27
+ :access_secret => credentials[:access_secret],
28
+ :require_secure => true
29
+ }
30
+ get("/users/#{user.id}/payments/templates", options)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,49 @@
1
+ module Etsy
2
+
3
+ # = Profile
4
+ #
5
+ # Represents a profile resource of an Etsy user.
6
+ #
7
+ class Profile
8
+
9
+ include Etsy::Model
10
+
11
+ attribute :id, :from => :user_profile_id
12
+ attribute :user_id
13
+ attribute :bio
14
+ attribute :username, :from => :login_name
15
+ attribute :gender
16
+ attribute :birth_day
17
+ attribute :birth_month
18
+ attribute :birth_year
19
+ attribute :joined, :from => :join_tsz
20
+ attribute :favorite_materials, :from => :materials
21
+ attribute :country_id
22
+ attribute :city
23
+ attribute :location
24
+ attribute :region
25
+ attribute :avatar_id
26
+ attribute :image, :from => :image_url_75x75
27
+ attribute :lat
28
+ attribute :lon
29
+ attribute :transaction_buy_count
30
+ attribute :transaction_sold_count
31
+ attribute :is_seller
32
+ attribute :first_name
33
+ attribute :last_name
34
+
35
+ def materials
36
+ favorite_materials ? favorite_materials.split(',') : []
37
+ end
38
+
39
+ # Time that this user joined Etsy
40
+ #
41
+ def joined_at
42
+ Time.at(joined)
43
+ end
44
+
45
+ def seller?
46
+ is_seller
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,148 @@
1
+ module Etsy
2
+
3
+ # = Request
4
+ #
5
+ # A basic wrapper around GET requests to the Etsy JSON API
6
+ #
7
+ class Request
8
+
9
+ # Perform a GET request for the resource with optional parameters - returns
10
+ # A Response object with the payload data
11
+ def self.get(resource_path, parameters = {})
12
+ request = Request.new(resource_path, parameters)
13
+ Response.new(request.get)
14
+ end
15
+
16
+ def self.post(resource_path, parameters = {})
17
+ request = Request.new(resource_path, parameters)
18
+ Response.new(request.post)
19
+ end
20
+
21
+ def self.put(resource_path, parameters = {})
22
+ request = Request.new(resource_path, parameters)
23
+ Response.new(request.put)
24
+ end
25
+
26
+ def self.delete(resource_path, parameters = {})
27
+ request = Request.new(resource_path, parameters)
28
+ Response.new(request.delete)
29
+ end
30
+
31
+ # Create a new request for the resource with optional parameters
32
+ def initialize(resource_path, parameters = {})
33
+ parameters = parameters.dup
34
+ @token = parameters.delete(:access_token) || Etsy.credentials[:access_token]
35
+ @secret = parameters.delete(:access_secret) || Etsy.credentials[:access_secret]
36
+ raise("Secure connection required. Please provide your OAuth credentials via :access_token and :access_secret in the parameters") if parameters.delete(:require_secure) && !secure?
37
+ @multipart_request = parameters.delete(:multipart)
38
+ @resource_path = resource_path
39
+ @resources = parameters.delete(:includes)
40
+ if @resources.class == String
41
+ @resources = @resources.split(',').map {|r| {:resource => r}}
42
+ elsif @resources.class == Array
43
+ @resources = @resources.map do |r|
44
+ if r.class == String
45
+ {:resource => r}
46
+ else
47
+ r
48
+ end
49
+ end
50
+ end
51
+ parameters = parameters.merge(:api_key => Etsy.api_key) unless secure?
52
+ @parameters = parameters
53
+ end
54
+
55
+ def base_path # :nodoc:
56
+ "/v2"
57
+ end
58
+
59
+ # Perform a GET request against the API endpoint and return the raw
60
+ # response data
61
+ def get
62
+ client.get(endpoint_url)
63
+ end
64
+
65
+ def post
66
+ if multipart?
67
+ client.post_multipart(endpoint_url(:include_query => false), @parameters)
68
+ else
69
+ client.post(endpoint_url)
70
+ end
71
+ end
72
+
73
+ def put
74
+ client.put(endpoint_url)
75
+ end
76
+
77
+ def delete
78
+ client.delete(endpoint_url)
79
+ end
80
+
81
+ def client # :nodoc:
82
+ @client ||= secure? ? secure_client : basic_client
83
+ end
84
+
85
+ def parameters # :nodoc:
86
+ @parameters
87
+ end
88
+
89
+ def resources # :nodoc:
90
+ @resources
91
+ end
92
+
93
+ def query # :nodoc:
94
+ to_url(parameters.merge(:includes => resources.to_a.map { |r| association(r) }))
95
+ end
96
+
97
+ def to_url(val)
98
+ if val.is_a? Array
99
+ to_url(val.join(','))
100
+ elsif val.is_a? Hash
101
+ val.reject { |k, v|
102
+ k.nil? || v.nil? || (k.respond_to?(:empty?) && k.empty?) || (v.respond_to?(:empty?) && v.empty?)
103
+ }.map { |k, v| "#{to_url(k.to_s)}=#{to_url(v)}" }.join('&')
104
+ else
105
+ URI.escape(val.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
106
+ end
107
+ end
108
+
109
+ def association(options={}) # :nodoc:
110
+ s = options[:resource].dup
111
+
112
+ if options.include? :fields
113
+ s << "(#{[options[:fields]].flatten.join(',')})"
114
+ end
115
+
116
+ if options.include?(:limit) || options.include?(:offset)
117
+ s << ":#{options.fetch(:limit, 25)}:#{options.fetch(:offset, 0)}"
118
+ end
119
+
120
+ s
121
+ end
122
+
123
+ def endpoint_url(options = {}) # :nodoc:
124
+ url = "#{base_path}#{@resource_path}"
125
+ url += "?#{query}" if options.fetch(:include_query, true)
126
+ url
127
+ end
128
+
129
+ def multipart?
130
+ !!@multipart_request
131
+ end
132
+
133
+ private
134
+
135
+ def secure_client
136
+ SecureClient.new(:access_token => @token, :access_secret => @secret)
137
+ end
138
+
139
+ def basic_client
140
+ BasicClient.new(Etsy.host)
141
+ end
142
+
143
+ def secure?
144
+ !@token.nil? && !@secret.nil?
145
+ end
146
+
147
+ end
148
+ end
@@ -0,0 +1,112 @@
1
+ module Etsy
2
+
3
+ class OAuthTokenRevoked < StandardError; end
4
+ class MissingShopID < StandardError; end
5
+ class EtsyJSONInvalid < StandardError; end
6
+ class TemporaryIssue < StandardError; end
7
+ class InvalidUserID < StandardError; end
8
+
9
+ # = Response
10
+ #
11
+ # Basic wrapper around the Etsy JSON response data
12
+ #
13
+ class Response
14
+
15
+ # Create a new response based on the raw HTTP response
16
+ def initialize(raw_response)
17
+ @raw_response = raw_response
18
+ end
19
+
20
+ # Convert the raw JSON data to a hash
21
+ def to_hash
22
+ validate!
23
+ @hash ||= json
24
+ end
25
+
26
+ def body
27
+ @raw_response.body
28
+ end
29
+
30
+ def code
31
+ @raw_response.code
32
+ end
33
+
34
+ # Number of records in the response results
35
+ def count
36
+ if paginated?
37
+ to_hash['results'].nil? ? 0 : to_hash['results'].size
38
+ else
39
+ to_hash['count']
40
+ end
41
+ end
42
+
43
+ # Results of the API request
44
+ def result
45
+ if success?
46
+ results = to_hash['results'] || []
47
+ count == 1 ? results.first : results
48
+ else
49
+ []
50
+ end
51
+ end
52
+
53
+ def success?
54
+ !!(code =~ /2\d\d/)
55
+ end
56
+
57
+ def paginated?
58
+ !!to_hash['pagination']
59
+ end
60
+
61
+ private
62
+
63
+ def data
64
+ @raw_response.body
65
+ end
66
+
67
+ def json
68
+ @hash ||= JSON.parse(data)
69
+ end
70
+
71
+ def validate!
72
+ raise OAuthTokenRevoked if token_revoked?
73
+ raise MissingShopID if missing_shop_id?
74
+ raise InvalidUserID if invalid_user_id?
75
+ raise TemporaryIssue if temporary_etsy_issue? || resource_unavailable? || exceeded_rate_limit?
76
+ raise EtsyJSONInvalid.new("CODE: #{code}, BODY: #{data}") unless valid_json?
77
+ true
78
+ end
79
+
80
+ def valid_json?
81
+ json
82
+ return true
83
+ rescue JSON::ParserError
84
+ return false
85
+ end
86
+
87
+ def token_revoked?
88
+ data == "oauth_problem=token_revoked"
89
+ end
90
+
91
+ def missing_shop_id?
92
+ data =~ /Shop with PK shop_id/
93
+ end
94
+
95
+ def invalid_user_id?
96
+ data =~ /is not a valid user_id/
97
+ end
98
+
99
+ def temporary_etsy_issue?
100
+ data =~ /Temporary Etsy issue/
101
+ end
102
+
103
+ def resource_unavailable?
104
+ data =~ /Resource temporarily unavailable/
105
+ end
106
+
107
+ def exceeded_rate_limit?
108
+ data =~ /You have exceeded/
109
+ end
110
+
111
+ end
112
+ end
@@ -0,0 +1,16 @@
1
+ module Etsy
2
+ class Section
3
+ include Etsy::Model
4
+
5
+ attributes :title, :rank, :user_id, :active_listing_count
6
+ attribute :id, :from => :shop_section_id
7
+
8
+ def self.find_by_shop(shop)
9
+ get("/shops/#{shop.id}/sections")
10
+ end
11
+
12
+ def self.find(shop, id)
13
+ get("/shops/#{shop.id}/sections/#{id}")
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,128 @@
1
+ module Etsy
2
+
3
+ # = SecureClient
4
+ #
5
+ # Used for generating tokens and calling API methods that require authentication.
6
+ #
7
+ class SecureClient
8
+
9
+ # Create a new client with the necessary parameters. Accepts the following
10
+ # key/value pairs:
11
+ #
12
+ # :request_token
13
+ # :request_secret
14
+ # :access_token
15
+ # :access_secret
16
+ #
17
+ # The request token / secret is useful for generating the access token. Pass
18
+ # the access token / secret when initializing from stored credentials.
19
+ #
20
+ def initialize(attributes = {})
21
+ @attributes = attributes
22
+ end
23
+
24
+ def consumer # :nodoc:
25
+ path = '/v2/oauth/'
26
+ @consumer ||= OAuth::Consumer.new(Etsy.api_key, Etsy.api_secret, {
27
+ :site => "http://#{Etsy.host}",
28
+ :request_token_path => "#{path}request_token?scope=#{Etsy.permission_scopes.join('+')}",
29
+ :access_token_path => "#{path}access_token"
30
+ })
31
+ end
32
+
33
+ # Generate a request token.
34
+ #
35
+ def request_token
36
+ consumer.get_request_token(:oauth_callback => Etsy.callback_url)
37
+ end
38
+
39
+ # Access token for this request, either generated from the request token or taken
40
+ # from the :access_token parameter.
41
+ #
42
+ def access_token
43
+ @attributes[:access_token] || client.token
44
+ end
45
+
46
+ # Access secret for this request, either generated from the request token or taken
47
+ # from the :access_secret parameter.
48
+ #
49
+ def access_secret
50
+ @attributes[:access_secret] || client.secret
51
+ end
52
+
53
+ def client_from_request_data # :nodoc:
54
+ request_token = OAuth::RequestToken.new(consumer, @attributes[:request_token], @attributes[:request_secret])
55
+ request_token.get_access_token(:oauth_verifier => @attributes[:verifier])
56
+ end
57
+
58
+ def client_from_access_data # :nodoc:
59
+ OAuth::AccessToken.new(consumer, @attributes[:access_token], @attributes[:access_secret])
60
+ end
61
+
62
+ def client # :nodoc:
63
+ @client ||= has_access_data? ? client_from_access_data : client_from_request_data
64
+ end
65
+
66
+ # Fetch a raw response from the specified endpoint.
67
+ #
68
+ def get(endpoint)
69
+ client.get(endpoint)
70
+ end
71
+
72
+ def post(endpoint)
73
+ client.post(endpoint)
74
+ end
75
+
76
+ def put(endpoint)
77
+ client.put(endpoint)
78
+ end
79
+
80
+ def delete(endpoint)
81
+ client.delete(endpoint)
82
+ end
83
+
84
+ def post_multipart(endpoint, params = {})
85
+ Net::HTTP.new(Etsy.host).start do |http|
86
+ req = Net::HTTP::Post.new(endpoint)
87
+ add_multipart_data(req, params)
88
+ add_oauth(req)
89
+ res = http.request(req)
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ # Encodes the request as multipart
96
+ def add_multipart_data(req, params)
97
+ crlf = "\r\n"
98
+ boundary = Time.now.to_i.to_s(16)
99
+ req["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
100
+ body = ""
101
+ params.each do |key,value|
102
+ esc_key = CGI.escape(key.to_s)
103
+ body << "--#{boundary}#{crlf}"
104
+ if value.respond_to?(:read)
105
+ body << "Content-Disposition: form-data; name=\"#{esc_key}\"; filename=\"#{File.basename(value.path)}\"#{crlf}"
106
+ body << "Content-Type: image/jpeg#{crlf*2}"
107
+ body << value.read
108
+ else
109
+ body << "Content-Disposition: form-data; name=\"#{esc_key}\"#{crlf*2}#{value}"
110
+ end
111
+ body << crlf
112
+ end
113
+ body << "--#{boundary}--#{crlf*2}"
114
+ req.body = body
115
+ req["Content-Length"] = req.body.size
116
+ end
117
+
118
+ # Uses the OAuth gem to add the signed Authorization header
119
+ def add_oauth(req)
120
+ client.sign!(req)
121
+ end
122
+
123
+ def has_access_data?
124
+ !@attributes[:access_token].nil? && !@attributes[:access_secret].nil?
125
+ end
126
+
127
+ end
128
+ end