cardmagic-etsy 0.3.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 (93) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.travis.yml +13 -0
  4. data/Gemfile +11 -0
  5. data/LICENSE +9 -0
  6. data/README.md +348 -0
  7. data/Rakefile +12 -0
  8. data/etsy.gemspec +34 -0
  9. data/lib/etsy.rb +223 -0
  10. data/lib/etsy/about.rb +15 -0
  11. data/lib/etsy/address.rb +47 -0
  12. data/lib/etsy/attribute_value.rb +46 -0
  13. data/lib/etsy/basic_client.rb +32 -0
  14. data/lib/etsy/category.rb +84 -0
  15. data/lib/etsy/country.rb +27 -0
  16. data/lib/etsy/favorite_listing.rb +26 -0
  17. data/lib/etsy/image.rb +44 -0
  18. data/lib/etsy/listing.rb +296 -0
  19. data/lib/etsy/model.rb +127 -0
  20. data/lib/etsy/payment_template.rb +33 -0
  21. data/lib/etsy/profile.rb +49 -0
  22. data/lib/etsy/receipt.rb +37 -0
  23. data/lib/etsy/request.rb +150 -0
  24. data/lib/etsy/response.rb +128 -0
  25. data/lib/etsy/section.rb +16 -0
  26. data/lib/etsy/secure_client.rb +131 -0
  27. data/lib/etsy/shipping_info.rb +27 -0
  28. data/lib/etsy/shipping_template.rb +41 -0
  29. data/lib/etsy/shop.rb +88 -0
  30. data/lib/etsy/transaction.rb +29 -0
  31. data/lib/etsy/user.rb +109 -0
  32. data/lib/etsy/variation/property_set.rb +71 -0
  33. data/lib/etsy/verification_request.rb +17 -0
  34. data/lib/etsy/version.rb +3 -0
  35. data/test/fixtures/about/getAbout.json +16 -0
  36. data/test/fixtures/address/getUserAddresses.json +12 -0
  37. data/test/fixtures/attribute_value/findAllListingPropertyValues.json +44 -0
  38. data/test/fixtures/category/findAllSubCategoryChildren.json +78 -0
  39. data/test/fixtures/category/findAllTopCategory.json +347 -0
  40. data/test/fixtures/category/findAllTopCategory.single.json +18 -0
  41. data/test/fixtures/category/findAllTopCategoryChildren.json +308 -0
  42. data/test/fixtures/category/getCategory.multiple.json +28 -0
  43. data/test/fixtures/category/getCategory.single.json +18 -0
  44. data/test/fixtures/country/getCountry.json +1 -0
  45. data/test/fixtures/favorite_listing/findAllFavoriteListings.json +1 -0
  46. data/test/fixtures/image/findAllListingImages.json +102 -0
  47. data/test/fixtures/listing/findAllListingActive.category.json +827 -0
  48. data/test/fixtures/listing/findAllShopListings.json +69 -0
  49. data/test/fixtures/listing/getListing.multiple.json +1 -0
  50. data/test/fixtures/listing/getListing.single.includeImages.json +1 -0
  51. data/test/fixtures/listing/getListing.single.json +1 -0
  52. data/test/fixtures/payment_template/getPaymentTemplate.json +1 -0
  53. data/test/fixtures/profile/new.json +28 -0
  54. data/test/fixtures/receipt/findAllShopReceipts.json +28 -0
  55. data/test/fixtures/section/getShopSection.json +18 -0
  56. data/test/fixtures/shipping_info/getShippingInfo.json +1 -0
  57. data/test/fixtures/shipping_template/getShippingTemplate.json +1 -0
  58. data/test/fixtures/shop/findAllShop.json +1 -0
  59. data/test/fixtures/shop/findAllShop.single.json +1 -0
  60. data/test/fixtures/shop/getShop.multiple.json +1 -0
  61. data/test/fixtures/shop/getShop.single.json +34 -0
  62. data/test/fixtures/transaction/findAllShopTransactions.json +1 -0
  63. data/test/fixtures/user/getUser.multiple.json +29 -0
  64. data/test/fixtures/user/getUser.single.json +13 -0
  65. data/test/fixtures/user/getUser.single.private.json +18 -0
  66. data/test/fixtures/user/getUser.single.withProfile.json +38 -0
  67. data/test/fixtures/user/getUser.single.withShops.json +41 -0
  68. data/test/test_helper.rb +45 -0
  69. data/test/unit/etsy/address_test.rb +61 -0
  70. data/test/unit/etsy/attribute_value_test.rb +67 -0
  71. data/test/unit/etsy/basic_client_test.rb +30 -0
  72. data/test/unit/etsy/category_test.rb +106 -0
  73. data/test/unit/etsy/country_test.rb +64 -0
  74. data/test/unit/etsy/favorite_listing_test.rb +44 -0
  75. data/test/unit/etsy/image_test.rb +51 -0
  76. data/test/unit/etsy/listing_test.rb +268 -0
  77. data/test/unit/etsy/model_test.rb +64 -0
  78. data/test/unit/etsy/payment_template_test.rb +68 -0
  79. data/test/unit/etsy/profile_test.rb +111 -0
  80. data/test/unit/etsy/receipt_test.rb +107 -0
  81. data/test/unit/etsy/request_test.rb +190 -0
  82. data/test/unit/etsy/response_test.rb +175 -0
  83. data/test/unit/etsy/section_test.rb +28 -0
  84. data/test/unit/etsy/secure_client_test.rb +132 -0
  85. data/test/unit/etsy/shipping_info_test.rb +24 -0
  86. data/test/unit/etsy/shipping_template_test.rb +24 -0
  87. data/test/unit/etsy/shop_about_test.rb +43 -0
  88. data/test/unit/etsy/shop_test.rb +116 -0
  89. data/test/unit/etsy/transaction_test.rb +61 -0
  90. data/test/unit/etsy/user_test.rb +250 -0
  91. data/test/unit/etsy/verification_request_test.rb +26 -0
  92. data/test/unit/etsy_test.rb +173 -0
  93. metadata +293 -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,37 @@
1
+ module Etsy
2
+ class Receipt
3
+ include Model
4
+
5
+ attribute :id, :from => :receipt_id
6
+ attribute :buyer_id, :from => :buyer_user_id
7
+
8
+ attributes :quantity, :listing_id, :name, :first_line, :second_line, :city, :state, :zip, :country_id,
9
+ :payment_email, :buyer_email, :creation_tsz
10
+
11
+ def self.find_all_by_shop_id(shop_id, options = {})
12
+ get_all("/shops/#{shop_id}/receipts", options)
13
+ end
14
+
15
+ def self.find_all_by_shop_id_and_status(shop_id, status, options = {})
16
+ get_all("/shops/#{shop_id}/receipts/#{status}", options)
17
+ end
18
+
19
+ def created_at
20
+ Time.at(creation_tsz)
21
+ end
22
+
23
+ def buyer
24
+ @buyer ||= User.find(buyer_id)
25
+ end
26
+
27
+ def transactions
28
+ unless @transactions
29
+ options = {}
30
+ options = options.merge(:access_token => token, :access_secret => secret) if (token && secret)
31
+ @transactions = Transaction.find_all_by_receipt_id(id, options)
32
+ end
33
+ @transactions
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,150 @@
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
+
32
+
33
+ # Create a new request for the resource with optional parameters
34
+ def initialize(resource_path, parameters = {})
35
+ parameters = parameters.dup
36
+ @token = parameters.delete(:access_token) || Etsy.credentials[:access_token]
37
+ @secret = parameters.delete(:access_secret) || Etsy.credentials[:access_secret]
38
+ raise("Secure connection required. Please provide your OAuth credentials via :access_token and :access_secret in the parameters") if parameters.delete(:require_secure) && !secure?
39
+ @multipart_request = parameters.delete(:multipart)
40
+ @resource_path = resource_path
41
+ @resources = parameters.delete(:includes)
42
+ if @resources.class == String
43
+ @resources = @resources.split(',').map {|r| {:resource => r}}
44
+ elsif @resources.class == Array
45
+ @resources = @resources.map do |r|
46
+ if r.class == String
47
+ {:resource => r}
48
+ else
49
+ r
50
+ end
51
+ end
52
+ end
53
+ parameters = parameters.merge(:api_key => Etsy.api_key) unless secure?
54
+ @parameters = parameters
55
+ end
56
+
57
+ def base_path # :nodoc:
58
+ "/v2"
59
+ end
60
+
61
+ # Perform a GET request against the API endpoint and return the raw
62
+ # response data
63
+ def get
64
+ client.get(endpoint_url)
65
+ end
66
+
67
+ def post
68
+ if multipart?
69
+ client.post_multipart(endpoint_url(:include_query => false), @parameters)
70
+ else
71
+ client.post(endpoint_url)
72
+ end
73
+ end
74
+
75
+ def put
76
+ client.put(endpoint_url)
77
+ end
78
+
79
+ def delete
80
+ client.delete(endpoint_url)
81
+ end
82
+
83
+ def client # :nodoc:
84
+ @client ||= secure? ? secure_client : basic_client
85
+ end
86
+
87
+ def parameters # :nodoc:
88
+ @parameters
89
+ end
90
+
91
+ def resources # :nodoc:
92
+ @resources
93
+ end
94
+
95
+ def query # :nodoc:
96
+ to_url(parameters.merge(:includes => resources.to_a.map { |r| association(r) }))
97
+ end
98
+
99
+ def to_url(val)
100
+ if val.is_a? Array
101
+ to_url(val.join(','))
102
+ elsif val.is_a? Hash
103
+ val.reject { |k, v|
104
+ k.nil? || v.nil? || (k.respond_to?(:empty?) && k.empty?) || (v.respond_to?(:empty?) && v.empty?)
105
+ }.map { |k, v| "#{to_url(k.to_s)}=#{to_url(v)}" }.join('&')
106
+ else
107
+ URI.escape(val.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
108
+ end
109
+ end
110
+
111
+ def association(options={}) # :nodoc:
112
+ s = options[:resource].dup
113
+
114
+ if options.include? :fields
115
+ s << "(#{[options[:fields]].flatten.join(',')})"
116
+ end
117
+
118
+ if options.include?(:limit) || options.include?(:offset)
119
+ s << ":#{options.fetch(:limit, 25)}:#{options.fetch(:offset, 0)}"
120
+ end
121
+
122
+ s
123
+ end
124
+
125
+ def endpoint_url(options = {}) # :nodoc:
126
+ url = "#{base_path}#{@resource_path}"
127
+ url += "?#{query}" if options.fetch(:include_query, true)
128
+ url
129
+ end
130
+
131
+ def multipart?
132
+ !!@multipart_request
133
+ end
134
+
135
+ private
136
+
137
+ def secure_client
138
+ SecureClient.new(:access_token => @token, :access_secret => @secret)
139
+ end
140
+
141
+ def basic_client
142
+ BasicClient.new
143
+ end
144
+
145
+ def secure?
146
+ !@token.nil? && !@secret.nil?
147
+ end
148
+
149
+ end
150
+ end
@@ -0,0 +1,128 @@
1
+ module Etsy
2
+
3
+ class OAuthTokenRevoked < StandardError; end
4
+ class MissingShopID < StandardError; end
5
+ class TemporaryIssue < StandardError; end
6
+ class ResourceUnavailable < TemporaryIssue; end
7
+ class ExceededRateLimit < TemporaryIssue; end
8
+ class InvalidUserID < StandardError; end
9
+ class EtsyJSONInvalid < StandardError
10
+ attr_reader :code, :data
11
+ def initialize(args)
12
+ @code = args[:code]
13
+ @data = args[:data]
14
+ end
15
+ end
16
+
17
+
18
+ # = Response
19
+ #
20
+ # Basic wrapper around the Etsy JSON response data
21
+ #
22
+ class Response
23
+
24
+ # Create a new response based on the raw HTTP response
25
+ def initialize(raw_response)
26
+ @raw_response = raw_response
27
+ end
28
+
29
+ # Convert the raw JSON data to a hash
30
+ def to_hash
31
+ validate!
32
+ @hash ||= json
33
+ end
34
+
35
+ def body
36
+ @raw_response.body
37
+ end
38
+
39
+ def code
40
+ @raw_response.code
41
+ end
42
+
43
+ # Number of records in the response results
44
+ def count
45
+ if paginated?
46
+ to_hash['results'].nil? ? 0 : to_hash['results'].size
47
+ else
48
+ to_hash['count']
49
+ end
50
+ end
51
+
52
+ # Results of the API request
53
+ def result
54
+ if success?
55
+ results = to_hash['results'] || []
56
+ count == 1 ? results.first : results
57
+ else
58
+ Etsy.silent_errors ? [] : validate!
59
+ end
60
+ end
61
+
62
+ # Total number of results of the request
63
+ def total
64
+ @total ||= to_hash['count']
65
+ end
66
+
67
+ def success?
68
+ !!(code =~ /2\d\d/)
69
+ end
70
+
71
+ def paginated?
72
+ !!to_hash['pagination']
73
+ end
74
+
75
+ private
76
+
77
+ def data
78
+ @raw_response.body
79
+ end
80
+
81
+ def json
82
+ @hash ||= JSON.parse(data)
83
+ end
84
+
85
+ def validate!
86
+ raise OAuthTokenRevoked if token_revoked?
87
+ raise MissingShopID if missing_shop_id?
88
+ raise InvalidUserID if invalid_user_id?
89
+ raise TemporaryIssue if temporary_etsy_issue?
90
+ raise ResourceUnavailable if resource_unavailable?
91
+ raise ExceededRateLimit if exceeded_rate_limit?
92
+ raise EtsyJSONInvalid.new({:code => code, :data => data}) unless valid_json?
93
+ true
94
+ end
95
+
96
+ def valid_json?
97
+ json
98
+ return true
99
+ rescue JSON::ParserError
100
+ return false
101
+ end
102
+
103
+ def token_revoked?
104
+ data == "oauth_problem=token_revoked"
105
+ end
106
+
107
+ def missing_shop_id?
108
+ data =~ /Shop with PK shop_id/
109
+ end
110
+
111
+ def invalid_user_id?
112
+ data =~ /is not a valid user_id/
113
+ end
114
+
115
+ def temporary_etsy_issue?
116
+ data =~ /Temporary Etsy issue/
117
+ end
118
+
119
+ def resource_unavailable?
120
+ data =~ /Resource temporarily unavailable/
121
+ end
122
+
123
+ def exceeded_rate_limit?
124
+ data =~ /You have exceeded/
125
+ end
126
+
127
+ end
128
+ 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,131 @@
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 => "#{Etsy.protocol}://#{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
+ client = Net::HTTP.new(Etsy.host, Etsy.protocol == "http" ? 80 : 443)
86
+ client.use_ssl = true if Etsy.protocol == "https"
87
+
88
+ client.start do |http|
89
+ req = Net::HTTP::Post.new(endpoint)
90
+ add_multipart_data(req, params)
91
+ add_oauth(req)
92
+ res = http.request(req)
93
+ end
94
+ end
95
+
96
+ private
97
+
98
+ # Encodes the request as multipart
99
+ def add_multipart_data(req, params)
100
+ crlf = "\r\n"
101
+ boundary = Time.now.to_i.to_s(16)
102
+ req["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
103
+ body = ""
104
+ params.each do |key,value|
105
+ esc_key = CGI.escape(key.to_s)
106
+ body << "--#{boundary}#{crlf}"
107
+ if value.respond_to?(:read)
108
+ body << "Content-Disposition: form-data; name=\"#{esc_key}\"; filename=\"#{File.basename(value.path)}\"#{crlf}"
109
+ body << "Content-Type: image/jpeg#{crlf*2}"
110
+ body << open(value.path, "rb") {|io| io.read}
111
+ else
112
+ body << "Content-Disposition: form-data; name=\"#{esc_key}\"#{crlf*2}#{value}"
113
+ end
114
+ body << crlf
115
+ end
116
+ body << "--#{boundary}--#{crlf*2}"
117
+ req.body = body
118
+ req["Content-Length"] = req.body.size
119
+ end
120
+
121
+ # Uses the OAuth gem to add the signed Authorization header
122
+ def add_oauth(req)
123
+ client.sign!(req)
124
+ end
125
+
126
+ def has_access_data?
127
+ !@attributes[:access_token].nil? && !@attributes[:access_secret].nil?
128
+ end
129
+
130
+ end
131
+ end