karatekit 0.1.0

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.
@@ -0,0 +1,78 @@
1
+ require 'karatekit/connection'
2
+ require 'karatekit/warnable'
3
+ require 'karatekit/arguments'
4
+ require 'karatekit/configurable'
5
+ require 'karatekit/authentication'
6
+ require 'karatekit/rate_limit'
7
+
8
+ require 'karatekit/client/locations'
9
+ require 'karatekit/client/products'
10
+ require 'karatekit/client/sessions'
11
+ require 'karatekit/client/events'
12
+ require 'karatekit/client/event_details'
13
+ require 'karatekit/client/event_parts'
14
+ require 'karatekit/client/posts'
15
+ require 'karatekit/client/instructors'
16
+ require 'karatekit/client/instructor_details'
17
+ require 'karatekit/client/instructor_groups'
18
+ require 'karatekit/client/instructor_group_members'
19
+ require 'karatekit/client/rate_limit'
20
+
21
+ module Karatekit
22
+
23
+ # Client for the kampfsport.center API
24
+ #
25
+ # @see https://developer.kampfsport.center
26
+ class Client
27
+
28
+ include Karatekit::Authentication
29
+ include Karatekit::Configurable
30
+ include Karatekit::Connection
31
+ include Karatekit::Warnable
32
+
33
+ include Karatekit::Client::Locations
34
+ include Karatekit::Client::Products
35
+ include Karatekit::Client::Sessions
36
+ include Karatekit::Client::Events
37
+ include Karatekit::Client::EventDetails
38
+ include Karatekit::Client::EventParts
39
+ include Karatekit::Client::Posts
40
+ include Karatekit::Client::Instructors
41
+ include Karatekit::Client::InstructorDetails
42
+ include Karatekit::Client::InstructorGroups
43
+ include Karatekit::Client::InstructorGroupMembers
44
+ include Karatekit::Client::RateLimit
45
+
46
+ # Header keys that can be passed in options hash to {#get},{#head}
47
+ CONVENIENCE_HEADERS = Set.new([:accept, :content_type])
48
+
49
+ def initialize(options = {})
50
+ # Use options passed in, but fall back to module defaults
51
+ Karatekit::Configurable.keys.each do |key|
52
+ value = options.key?(key) ? options[key] : Karatekit.instance_variable_get(:"@#{key}")
53
+ instance_variable_set(:"@#{key}", value)
54
+ end
55
+ end
56
+
57
+ # Text representation of the client, masking tokens and passwords
58
+ #
59
+ # @return [String]
60
+ def inspect
61
+ inspected = super
62
+
63
+ if @access_token
64
+ inspected = inspected.gsub! @access_token, "#{'*'*36}#{@access_token[36..-1]}"
65
+ end
66
+
67
+ inspected
68
+ end
69
+
70
+ # Set OAuth access token for authentication
71
+ #
72
+ # @param value [String] 40 character kampfsport.center OAuth access token
73
+ def access_token=(value)
74
+ reset_agent
75
+ @access_token = value
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,80 @@
1
+ module Karatekit
2
+
3
+ # Configuration options for {Client}, defaulting to values
4
+ # in {Default}
5
+ module Configurable
6
+ # @!attribute [w] access_token
7
+ # @see https://developer.github.com/v3/oauth/
8
+ # @return [String] OAuth2 access token for authentication
9
+ # @!attribute api_endpoint
10
+ # @return [String] Base URL for API requests. default: https://api.github.com/
11
+ # @!attribute auto_paginate
12
+ # @return [Boolean] Auto fetch next page of results until rate limit reached
13
+ # @!attribute connection_options
14
+ # @see https://github.com/lostisland/faraday
15
+ # @return [Hash] Configure connection options for Faraday
16
+ # @!attribute middleware
17
+ # @see https://github.com/lostisland/faraday
18
+ # @return [Faraday::Builder or Faraday::RackBuilder] Configure middleware for Faraday
19
+ # @!attribute per_page
20
+ # @return [String] Configure page size for paginated results. API default: 30
21
+ # @!attribute user_agent
22
+ # @return [String] Configure User-Agent header for requests.
23
+
24
+ attr_accessor :access_token, :auto_paginate,
25
+ :connection_options, :default_media_type,
26
+ :middleware,
27
+ :per_page, :user_agent
28
+ attr_writer :api_endpoint
29
+
30
+ class << self
31
+
32
+ # List of configurable keys for {Karatekit::Client}
33
+ # @return [Array] of option keys
34
+ def keys
35
+ @keys ||= [
36
+ :access_token,
37
+ :api_endpoint,
38
+ :default_media_type,
39
+ :auto_paginate,
40
+ :connection_options,
41
+ :middleware,
42
+ :per_page,
43
+ :user_agent,
44
+ ]
45
+ end
46
+ end
47
+
48
+ # Set configuration options using a block
49
+ def configure
50
+ yield self
51
+ end
52
+
53
+ # Reset configuration options to default values
54
+ def reset!
55
+ Karatekit::Configurable.keys.each do |key|
56
+ instance_variable_set(:"@#{key}", Karatekit::Default.options[key])
57
+ end
58
+ self
59
+ end
60
+ alias setup reset!
61
+
62
+ # Compares client options to a Hash of requested options
63
+ #
64
+ # @param opts [Hash] Options to compare with current client options
65
+ # @return [Boolean]
66
+ def same_options?(opts)
67
+ opts.hash == options.hash
68
+ end
69
+
70
+ def api_endpoint
71
+ File.join(@api_endpoint, "")
72
+ end
73
+
74
+ private
75
+
76
+ def options
77
+ Hash[Karatekit::Configurable.keys.map{|key| [key, instance_variable_get(:"@#{key}")]}]
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,191 @@
1
+ require 'sawyer'
2
+ require 'karatekit/authentication'
3
+ module Karatekit
4
+
5
+ # Network layer for API clients.
6
+ module Connection
7
+
8
+ include Karatekit::Authentication
9
+
10
+ # Header keys that can be passed in options hash to {#get},{#head}
11
+ CONVENIENCE_HEADERS = Set.new([:accept, :content_type])
12
+
13
+ # Make a HTTP GET request
14
+ #
15
+ # @param url [String] The path, relative to {#api_endpoint}
16
+ # @param options [Hash] Query and header params for request
17
+ # @return [Sawyer::Resource]
18
+ def get(url, options = {})
19
+ request :get, url, parse_query_and_convenience_headers(options)
20
+ end
21
+
22
+ # Make a HTTP POST request
23
+ #
24
+ # @param url [String] The path, relative to {#api_endpoint}
25
+ # @param options [Hash] Body and header params for request
26
+ # @return [Sawyer::Resource]
27
+ def post(url, options = {})
28
+ request :post, url, options
29
+ end
30
+
31
+ # Make a HTTP PUT request
32
+ #
33
+ # @param url [String] The path, relative to {#api_endpoint}
34
+ # @param options [Hash] Body and header params for request
35
+ # @return [Sawyer::Resource]
36
+ def put(url, options = {})
37
+ request :put, url, options
38
+ end
39
+
40
+ # Make a HTTP PATCH request
41
+ #
42
+ # @param url [String] The path, relative to {#api_endpoint}
43
+ # @param options [Hash] Body and header params for request
44
+ # @return [Sawyer::Resource]
45
+ def patch(url, options = {})
46
+ request :patch, url, options
47
+ end
48
+
49
+ # Make a HTTP DELETE request
50
+ #
51
+ # @param url [String] The path, relative to {#api_endpoint}
52
+ # @param options [Hash] Query and header params for request
53
+ # @return [Sawyer::Resource]
54
+ def delete(url, options = {})
55
+ request :delete, url, options
56
+ end
57
+
58
+ # Make a HTTP HEAD request
59
+ #
60
+ # @param url [String] The path, relative to {#api_endpoint}
61
+ # @param options [Hash] Query and header params for request
62
+ # @return [Sawyer::Resource]
63
+ def head(url, options = {})
64
+ request :head, url, parse_query_and_convenience_headers(options)
65
+ end
66
+
67
+ # Make one or more HTTP GET requests, optionally fetching
68
+ # the next page of results from URL in Link response header based
69
+ # on value in {#auto_paginate}.
70
+ #
71
+ # @param url [String] The path, relative to {#api_endpoint}
72
+ # @param options [Hash] Query and header params for request
73
+ # @param block [Block] Block to perform the data concatination of the
74
+ # multiple requests. The block is called with two parameters, the first
75
+ # contains the contents of the requests so far and the second parameter
76
+ # contains the latest response.
77
+ # @return [Sawyer::Resource]
78
+ def paginate(url, options = {}, &block)
79
+ opts = parse_query_and_convenience_headers(options)
80
+ if @auto_paginate || @per_page
81
+ opts[:query][:per_page] ||= @per_page || (@auto_paginate ? 100 : nil)
82
+ end
83
+
84
+ data = request(:get, url, opts.dup)
85
+
86
+ if @auto_paginate
87
+ while @last_response.rels[:next] && rate_limit.remaining > 0
88
+ @last_response = @last_response.rels[:next].get(:headers => opts[:headers])
89
+ if block_given?
90
+ yield(data, @last_response)
91
+ else
92
+ data.concat(@last_response.data) if @last_response.data.is_a?(Array)
93
+ end
94
+ end
95
+
96
+ end
97
+
98
+ data
99
+ end
100
+
101
+ # Hypermedia agent for the kampfsport.center API
102
+ #
103
+ # @return [Sawyer::Agent]
104
+ def agent
105
+ @agent ||= Sawyer::Agent.new(endpoint, sawyer_options) do |http|
106
+ http.headers[:accept] = default_media_type
107
+ http.headers[:content_type] = "application/json"
108
+ http.headers[:user_agent] = user_agent
109
+ http.authorization 'Token', @access_token
110
+ end
111
+ end
112
+
113
+ # Fetch the root resource for the API
114
+ #
115
+ # @return [Sawyer::Resource]
116
+ def root
117
+ get "/"
118
+ end
119
+
120
+ # Response for last HTTP request
121
+ #
122
+ # @return [Sawyer::Response]
123
+ def last_response
124
+ @last_response if defined? @last_response
125
+ end
126
+
127
+ protected
128
+
129
+ def endpoint
130
+ api_endpoint
131
+ end
132
+
133
+ private
134
+
135
+ def reset_agent
136
+ @agent = nil
137
+ end
138
+
139
+ def request(method, path, data, options = {})
140
+ if data.is_a?(Hash)
141
+ options[:query] = data.delete(:query) || {}
142
+ options[:headers] = data.delete(:headers) || {}
143
+ if accept = data.delete(:accept)
144
+ options[:headers][:accept] = accept
145
+ end
146
+ end
147
+
148
+ @last_response = response = agent.call(method, Addressable::URI.parse(path.to_s).normalize.to_s, data, options)
149
+ response.data
150
+ end
151
+
152
+ # Executes the request, checking if it was successful
153
+ #
154
+ # @return [Boolean] True on success, false otherwise
155
+ def boolean_from_response(method, path, options = {})
156
+ request(method, path, options)
157
+ @last_response.status == 204
158
+ rescue Karatekit::NotFound
159
+ false
160
+ end
161
+
162
+ def sawyer_options
163
+ opts = {
164
+ :links_parser => Sawyer::LinkParsers::Simple.new
165
+ }
166
+ conn_opts = @connection_options
167
+ conn_opts[:builder] = @middleware if @middleware
168
+ conn_opts[:proxy] = @proxy if @proxy
169
+ conn_opts[:ssl] = { :verify_mode => @ssl_verify_mode } if @ssl_verify_mode
170
+ opts[:faraday] = Faraday.new(conn_opts)
171
+
172
+ opts
173
+ end
174
+
175
+ def parse_query_and_convenience_headers(options)
176
+ options = options.dup
177
+ headers = options.delete(:headers) { Hash.new }
178
+ CONVENIENCE_HEADERS.each do |h|
179
+ if header = options.delete(h)
180
+ headers[h] = header
181
+ end
182
+ end
183
+ query = options.delete(:query)
184
+ opts = {:query => options}
185
+ opts[:query].merge!(query) if query && query.is_a?(Hash)
186
+ opts[:headers] = headers unless headers.empty?
187
+
188
+ opts
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,97 @@
1
+ # require 'karatekit/middleware/follow_redirects'
2
+ require 'karatekit/response/raise_error'
3
+ # require 'karatekit/response/feed_parser'
4
+ require 'karatekit/version'
5
+
6
+ module Karatekit
7
+
8
+ # Default configuration options for {Client}
9
+ module Default
10
+
11
+ # Default API endpoint
12
+ API_ENDPOINT = "https://api.kampfsport.center".freeze
13
+
14
+ # Default User Agent header string
15
+ USER_AGENT = "Karatekit Ruby Gem #{Karatekit::VERSION}".freeze
16
+
17
+ # Default media type
18
+ MEDIA_TYPE = "application/json".freeze
19
+
20
+ # In Faraday 0.9, Faraday::Builder was renamed to Faraday::RackBuilder
21
+ RACK_BUILDER_CLASS = defined?(Faraday::RackBuilder) ? Faraday::RackBuilder : Faraday::Builder
22
+
23
+ # Default Faraday middleware stack
24
+ MIDDLEWARE = RACK_BUILDER_CLASS.new do |builder|
25
+ builder.use Faraday::Request::Retry, exceptions: [Karatekit::ServerError]
26
+ # builder.use Karatekit::Middleware::FollowRedirects
27
+ builder.use Karatekit::Response::RaiseError
28
+ # builder.use Karatekit::Response::FeedParser
29
+ builder.adapter Faraday.default_adapter
30
+ end
31
+
32
+ class << self
33
+
34
+ # Configuration options
35
+ # @return [Hash]
36
+ def options
37
+ Hash[Karatekit::Configurable.keys.map{|key| [key, send(key)]}]
38
+ end
39
+
40
+ # Default access token from ENV
41
+ # @return [String]
42
+ def access_token
43
+ ENV['KARATEKIT_ACCESS_TOKEN']
44
+ end
45
+
46
+ # Default API endpoint from ENV or {API_ENDPOINT}
47
+ # @return [String]
48
+ def api_endpoint
49
+ ENV['KARATEKIT_API_ENDPOINT'] || API_ENDPOINT
50
+ end
51
+
52
+ # Default pagination preference from ENV
53
+ # @return [String]
54
+ def auto_paginate
55
+ ENV['KARATEKIT_AUTO_PAGINATE']
56
+ end
57
+
58
+ # Default options for Faraday::Connection
59
+ # @return [Hash]
60
+ def connection_options
61
+ {
62
+ :headers => {
63
+ :accept => default_media_type,
64
+ :user_agent => user_agent
65
+ }
66
+ }
67
+ end
68
+
69
+ # Default media type from {MEDIA_TYPE}
70
+ # @return [String]
71
+ def default_media_type
72
+ MEDIA_TYPE
73
+ end
74
+
75
+ # Default middleware stack for Faraday::Connection
76
+ # from {MIDDLEWARE}
77
+ # @return [Faraday::RackBuilder or Faraday::Builder]
78
+ def middleware
79
+ MIDDLEWARE
80
+ end
81
+
82
+ # Default pagination page size from ENV
83
+ # @return [Integer] Page size
84
+ def per_page
85
+ page_size = ENV['KARATEKIT_PER_PAGE']
86
+
87
+ page_size.to_i if page_size
88
+ end
89
+
90
+ # Default User-Agent header string from ENV or {USER_AGENT}
91
+ # @return [String]
92
+ def user_agent
93
+ ENV['KARATEKIT_USER_AGENT'] || USER_AGENT
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,244 @@
1
+ module Karatekit
2
+ # Custom error class for rescuing from all kampfsport.center errors
3
+ class Error < StandardError
4
+
5
+ # Returns the appropriate Karatekit::Error subclass based
6
+ # on status and response message
7
+ #
8
+ # @param [Hash] response HTTP response
9
+ # @return [Karatekit::Error]
10
+ def self.from_response(response)
11
+ status = response[:status].to_i
12
+ body = response[:body].to_s
13
+ headers = response[:response_headers]
14
+
15
+ if klass = case status
16
+ when 400 then Karatekit::BadRequest
17
+ when 401 then error_for_401(headers)
18
+ when 403 then error_for_403(body)
19
+ when 404 then error_for_404(body)
20
+ when 405 then Karatekit::MethodNotAllowed
21
+ when 406 then Karatekit::NotAcceptable
22
+ when 409 then Karatekit::Conflict
23
+ when 415 then Karatekit::UnsupportedMediaType
24
+ when 422 then Karatekit::UnprocessableEntity
25
+ when 451 then Karatekit::UnavailableForLegalReasons
26
+ when 400..499 then Karatekit::ClientError
27
+ when 500 then Karatekit::InternalServerError
28
+ when 501 then Karatekit::NotImplemented
29
+ when 502 then Karatekit::BadGateway
30
+ when 503 then Karatekit::ServiceUnavailable
31
+ when 500..599 then Karatekit::ServerError
32
+ end
33
+ klass.new(response)
34
+ end
35
+ end
36
+
37
+ def initialize(response=nil)
38
+ @response = response
39
+ super(build_error_message)
40
+ end
41
+
42
+ # Documentation URL returned by the API for some errors
43
+ #
44
+ # @return [String]
45
+ def documentation_url
46
+ data[:documentation_url] if data.is_a? Hash
47
+ end
48
+
49
+ # Returns most appropriate error for 401 HTTP status code
50
+ # @private
51
+ def self.error_for_401(headers)
52
+ Karatekit::Unauthorized
53
+ end
54
+
55
+ # Returns most appropriate error for 403 HTTP status code
56
+ # @private
57
+ def self.error_for_403(body)
58
+ if body =~ /rate limit exceeded/i
59
+ Karatekit::TooManyRequests
60
+ elsif body =~ /login attempts exceeded/i
61
+ Karatekit::TooManyLoginAttempts
62
+ elsif body =~ /abuse/i
63
+ Karatekit::AbuseDetected
64
+ elsif body =~ /email address must be verified/i
65
+ Karatekit::UnverifiedEmail
66
+ elsif body =~ /account was suspended/i
67
+ Karatekit::AccountSuspended
68
+ else
69
+ Karatekit::Forbidden
70
+ end
71
+ end
72
+
73
+ # Return most appropriate error for 404 HTTP status code
74
+ # @private
75
+ def self.error_for_404(body)
76
+ Karatekit::NotFound
77
+ end
78
+
79
+ # Array of validation errors
80
+ # @return [Array<Hash>] Error info
81
+ def errors
82
+ if data && data.is_a?(Hash)
83
+ data[:errors] || []
84
+ else
85
+ []
86
+ end
87
+ end
88
+
89
+ # Status code returned by the kampfsport.center server.
90
+ #
91
+ # @return [Integer]
92
+ def response_status
93
+ @response[:status]
94
+ end
95
+
96
+ # Headers returned by the kampfsport.center server.
97
+ #
98
+ # @return [Hash]
99
+ def response_headers
100
+ @response[:response_headers]
101
+ end
102
+
103
+ # Body returned by the kampfsport.center server.
104
+ #
105
+ # @return [String]
106
+ def response_body
107
+ @response[:body]
108
+ end
109
+
110
+ private
111
+
112
+ def data
113
+ @data ||=
114
+ if (body = @response[:body]) && !body.empty?
115
+ if body.is_a?(String) &&
116
+ @response[:response_headers] &&
117
+ @response[:response_headers][:content_type] =~ /json/
118
+
119
+ Sawyer::Agent.serializer.decode(body)
120
+ else
121
+ body
122
+ end
123
+ else
124
+ nil
125
+ end
126
+ end
127
+
128
+ def response_message
129
+ case data
130
+ when Hash
131
+ data[:message]
132
+ when String
133
+ data
134
+ end
135
+ end
136
+
137
+ def response_error
138
+ "Error: #{data[:error]}" if data.is_a?(Hash) && data[:error]
139
+ end
140
+
141
+ def response_error_summary
142
+ return nil unless data.is_a?(Hash) && !Array(data[:errors]).empty?
143
+
144
+ summary = "\nError summary:\n"
145
+ summary << data[:errors].map do |error|
146
+ if error.is_a? Hash
147
+ error.map { |k,v| " #{k}: #{v}" }
148
+ else
149
+ " #{error}"
150
+ end
151
+ end.join("\n")
152
+
153
+ summary
154
+ end
155
+
156
+ def build_error_message
157
+ return nil if @response.nil?
158
+
159
+ message = "#{@response[:method].to_s.upcase} "
160
+ message << redact_url(@response[:url].to_s) + ": "
161
+ message << "#{@response[:status]} - "
162
+ message << "#{response_message}" unless response_message.nil?
163
+ message << "#{response_error}" unless response_error.nil?
164
+ message << "#{response_error_summary}" unless response_error_summary.nil?
165
+ message << " // See: #{documentation_url}" unless documentation_url.nil?
166
+ message
167
+ end
168
+
169
+ def redact_url(url_string)
170
+ %w[client_secret access_token].each do |token|
171
+ url_string.gsub!(/#{token}=\S+/, "#{token}=(redacted)") if url_string.include? token
172
+ end
173
+ url_string
174
+ end
175
+ end
176
+
177
+ # Raised on errors in the 400-499 range
178
+ class ClientError < Error; end
179
+
180
+ # Raised when kampfsport.center returns a 400 HTTP status code
181
+ class BadRequest < ClientError; end
182
+
183
+ # Raised when kampfsport.center returns a 401 HTTP status code
184
+ class Unauthorized < ClientError; end
185
+
186
+ # Raised when kampfsport.center returns a 403 HTTP status code
187
+ class Forbidden < ClientError; end
188
+
189
+ # Raised when kampfsport.center returns a 403 HTTP status code
190
+ # and body matches 'rate limit exceeded'
191
+ class TooManyRequests < Forbidden; end
192
+
193
+ # Raised when kampfsport.center returns a 403 HTTP status code
194
+ # and body matches 'login attempts exceeded'
195
+ class TooManyLoginAttempts < Forbidden; end
196
+
197
+ # Raised when kampfsport.center returns a 403 HTTP status code
198
+ # and body matches 'abuse'
199
+ class AbuseDetected < Forbidden; end
200
+
201
+ # Raised when kampfsport.center returns a 403 HTTP status code
202
+ # and body matches 'email address must be verified'
203
+ class UnverifiedEmail < Forbidden; end
204
+
205
+ # Raised when kampfsport.center returns a 403 HTTP status code
206
+ # and body matches 'account was suspended'
207
+ class AccountSuspended < Forbidden; end
208
+
209
+ # Raised when kampfsport.center returns a 404 HTTP status code
210
+ class NotFound < ClientError; end
211
+
212
+ # Raised when kampfsport.center returns a 405 HTTP status code
213
+ class MethodNotAllowed < ClientError; end
214
+
215
+ # Raised when kampfsport.center returns a 406 HTTP status code
216
+ class NotAcceptable < ClientError; end
217
+
218
+ # Raised when kampfsport.center returns a 409 HTTP status code
219
+ class Conflict < ClientError; end
220
+
221
+ # Raised when kampfsport.center returns a 414 HTTP status code
222
+ class UnsupportedMediaType < ClientError; end
223
+
224
+ # Raised when kampfsport.center returns a 422 HTTP status code
225
+ class UnprocessableEntity < ClientError; end
226
+
227
+ # Raised on errors in the 500-599 range
228
+ class ServerError < Error; end
229
+
230
+ # Raised when kampfsport.center returns a 500 HTTP status code
231
+ class InternalServerError < ServerError; end
232
+
233
+ # Raised when kampfsport.center returns a 501 HTTP status code
234
+ class NotImplemented < ServerError; end
235
+
236
+ # Raised when kampfsport.center returns a 502 HTTP status code
237
+ class BadGateway < ServerError; end
238
+
239
+ # Raised when kampfsport.center returns a 503 HTTP status code
240
+ class ServiceUnavailable < ServerError; end
241
+
242
+ # Raised when client fails to provide valid Content-Type
243
+ class MissingContentType < ArgumentError; end
244
+ end