looker-sdk 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +52 -0
  3. data/.ruby-gemset +1 -0
  4. data/.travis.yml +16 -0
  5. data/Gemfile +22 -0
  6. data/LICENSE +21 -0
  7. data/Rakefile +37 -0
  8. data/authentication.md +104 -0
  9. data/examples/add_delete_users.rb +94 -0
  10. data/examples/change_credentials_email_address_for_users.rb +23 -0
  11. data/examples/create_credentials_email_for_users.rb +19 -0
  12. data/examples/delete_all_user_sessions.rb +15 -0
  13. data/examples/delete_credentials_google_for_users.rb +19 -0
  14. data/examples/generate_password_reset_tokens_for_users.rb +19 -0
  15. data/examples/ldap_roles_test.rb +50 -0
  16. data/examples/me.rb +3 -0
  17. data/examples/refresh_user_notification_addresses.rb +10 -0
  18. data/examples/roles_and_users_with_permission.rb +22 -0
  19. data/examples/sdk_setup.rb +21 -0
  20. data/examples/streaming_downloads.rb +20 -0
  21. data/examples/users_with_credentials_email.rb +6 -0
  22. data/examples/users_with_credentials_google.rb +8 -0
  23. data/examples/users_with_credentials_google_without_credentials_email.rb +6 -0
  24. data/lib/looker-sdk.rb +32 -0
  25. data/lib/looker-sdk/authentication.rb +104 -0
  26. data/lib/looker-sdk/client.rb +445 -0
  27. data/lib/looker-sdk/client/dynamic.rb +107 -0
  28. data/lib/looker-sdk/configurable.rb +116 -0
  29. data/lib/looker-sdk/default.rb +148 -0
  30. data/lib/looker-sdk/error.rb +235 -0
  31. data/lib/looker-sdk/rate_limit.rb +33 -0
  32. data/lib/looker-sdk/response/raise_error.rb +20 -0
  33. data/lib/looker-sdk/sawyer_patch.rb +33 -0
  34. data/lib/looker-sdk/version.rb +7 -0
  35. data/looker-sdk.gemspec +27 -0
  36. data/readme.md +117 -0
  37. data/shell/.gitignore +41 -0
  38. data/shell/Gemfile +6 -0
  39. data/shell/readme.md +18 -0
  40. data/shell/shell.rb +37 -0
  41. data/streaming.md +59 -0
  42. data/test/helper.rb +46 -0
  43. data/test/looker/swagger.json +1998 -0
  44. data/test/looker/test_client.rb +258 -0
  45. data/test/looker/test_dynamic_client.rb +158 -0
  46. data/test/looker/test_dynamic_client_agent.rb +131 -0
  47. data/test/looker/user.json +1 -0
  48. metadata +107 -0
@@ -0,0 +1,107 @@
1
+ module LookerSDK
2
+ class Client
3
+
4
+ module Dynamic
5
+
6
+ attr_accessor :dynamic
7
+
8
+ def try_load_swagger
9
+ resp = get('swagger.json') rescue nil
10
+ resp && last_response && last_response.status == 200 && last_response.data && last_response.data.to_attrs
11
+ end
12
+
13
+ # If a given client is created with ':shared_swagger => true' then it will try to
14
+ # use a globally sharable @@operations hash built from one fetch of the swagger.json for the
15
+ # given api_endpoint. This is an optimization for the cases where many sdk clients get created and
16
+ # destroyed (perhaps with different access tokens) while all talking to the same endpoints. This cuts
17
+ # down overhead for such cases considerably.
18
+
19
+ @@sharable_operations = Hash.new
20
+
21
+ def clear_swagger
22
+ @swagger = @operations = nil
23
+ end
24
+
25
+ def load_swagger
26
+ # We only need the swagger if we are going to be building our own 'operations' hash
27
+ return if shared_swagger && @@sharable_operations[api_endpoint]
28
+ # Try to load w/o authenticating. Else, authenticate and try again.
29
+ @swagger ||= without_authentication {try_load_swagger} || try_load_swagger
30
+ end
31
+
32
+ def operations
33
+ return @@sharable_operations[api_endpoint] if shared_swagger && @@sharable_operations[api_endpoint]
34
+
35
+ return nil unless @swagger
36
+ @operations ||= Hash[
37
+ @swagger[:paths].map do |path_name, path_info|
38
+ path_info.map do |method, route_info|
39
+ route = @swagger[:basePath].to_s + path_name.to_s
40
+ [route_info[:operationId], {:route => route, :method => method, :info => route_info}]
41
+ end
42
+ end.reduce(:+)
43
+ ].freeze
44
+
45
+ shared_swagger ? (@@sharable_operations[api_endpoint] = @operations) : @operations
46
+ end
47
+
48
+ def method_link(entry)
49
+ uri = URI.parse(api_endpoint)
50
+ "#{uri.scheme}://#{uri.host}:#{uri.port}/api-docs/index.html#!/#{entry[:info][:tags].first}/#{entry[:info][:operationId]}" rescue "http://docs.looker.com/"
51
+ end
52
+
53
+ # Callers can explicitly 'invoke' remote methods or let 'method_missing' do the trick.
54
+ # If nothing else, this gives clients a way to deal with potential conflicts between remote method
55
+ # names and names of methods on client itself.
56
+ def invoke(method_name, *args, &block)
57
+ entry = find_entry(method_name) || raise(NameError, "undefined remote method '#{method_name}'")
58
+ invoke_remote(entry, method_name, *args, &block)
59
+ end
60
+
61
+ def method_missing(method_name, *args, &block)
62
+ entry = find_entry(method_name) || (return super)
63
+ invoke_remote(entry, method_name, *args, &block)
64
+ end
65
+
66
+ def respond_to?(method_name, include_private=false)
67
+ !!find_entry(method_name) || super
68
+ end
69
+
70
+ private
71
+
72
+ def find_entry(method_name)
73
+ operations && operations[method_name.to_s] if dynamic
74
+ end
75
+
76
+ def invoke_remote(entry, method_name, *args, &block)
77
+ args = (args || []).dup
78
+ route = entry[:route].to_s.dup
79
+ params = (entry[:info][:parameters] || []).select {|param| param[:in] == 'path'}
80
+ body_param = (entry[:info][:parameters] || []).select {|param| param[:in] == 'body'}.first
81
+
82
+ params_passed = args.length
83
+ params_required = params.length + (body_param && body_param[:required] ? 1 : 0)
84
+ unless params_passed >= params_required
85
+ raise ArgumentError.new("wrong number of arguments (#{params_passed} for #{params_required}) in call to '#{method_name}'. See '#{method_link(entry)}'")
86
+ end
87
+
88
+ # substitute the actual params into the route template
89
+ params.each {|param| route.sub!("{#{param[:name]}}", args.shift.to_s) }
90
+
91
+ a = args.length > 0 ? args[0] : {}
92
+ b = args.length > 1 ? args[1] : {}
93
+
94
+ method = entry[:method].to_sym
95
+ case method
96
+ when :get then get(route, a, &block)
97
+ when :post then post(route, a, merge_content_type_if_body(a, b), &block)
98
+ when :put then put(route, a, merge_content_type_if_body(a, b), &block)
99
+ when :patch then patch(route, a, merge_content_type_if_body(a, b), &block)
100
+ when :delete then delete(route, a) ; @raw_responses ? last_response : delete_succeeded?
101
+ else raise "unsupported method '#{method}' in call to '#{method_name}'. See '#{method_link(entry)}'"
102
+ end
103
+ end
104
+
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,116 @@
1
+ module LookerSDK
2
+
3
+ # Configuration options for {Client}, defaulting to values
4
+ # in {Default}
5
+ module Configurable
6
+ # @!attribute [w] access_token
7
+ # @see look TODO docs link
8
+ # @return [String] OAuth2 access token for authentication
9
+ # @!attribute api_endpoint
10
+ # @return [String] Base URL for API requests. default: https://api.looker.com/ look TODO: this is the wrong url... what's the right one? Also update all other references to "api.looker.com"
11
+ # @!attribute auto_paginate
12
+ # @return [Boolean] Auto fetch next page of results until rate limit reached
13
+ # @!attribute client_id
14
+ # @see look TODO docs link
15
+ # @return [String] Configure OAuth app key
16
+ # @!attribute [w] client_secret
17
+ # @see look TODO docs link
18
+ # @return [String] Configure OAuth app secret
19
+ # @!attribute default_media_type
20
+ # @see look TODO docs link
21
+ # @return [String] Configure preferred media type (for API versioning, for example)
22
+ # @!attribute connection_options
23
+ # @see https://github.com/lostisland/faraday
24
+ # @return [Hash] Configure connection options for Faraday
25
+ # @!attribute middleware
26
+ # @see https://github.com/lostisland/faraday
27
+ # @return [Faraday::Builder or Faraday::RackBuilder] Configure middleware for Faraday
28
+ # @!attribute netrc
29
+ # @return [Boolean] Instruct Looker to get credentials from .netrc file
30
+ # @!attribute netrc_file
31
+ # @return [String] Path to .netrc file. default: ~/.netrc
32
+ # @!attribute per_page
33
+ # @return [String] Configure page size for paginated results. API default: 30
34
+ # @!attribute proxy
35
+ # @see https://github.com/lostisland/faraday
36
+ # @return [String] URI for proxy server
37
+ # @!attribute user_agent
38
+ # @return [String] Configure User-Agent header for requests.
39
+ # @!attribute web_endpoint
40
+ # @return [String] Base URL for web URLs. default: https://<client>.looker.com/ look TODO is this correct?
41
+
42
+ attr_accessor :access_token, :auto_paginate, :client_id,
43
+ :client_secret, :default_media_type, :connection_options,
44
+ :middleware, :netrc, :netrc_file,
45
+ :per_page, :proxy, :user_agent, :faraday, :swagger, :shared_swagger, :raw_responses
46
+ attr_writer :web_endpoint, :api_endpoint
47
+
48
+ class << self
49
+
50
+ # List of configurable keys for {LookerSDK::Client}
51
+ # @return [Array] of option keys
52
+ def keys
53
+ @keys ||= [
54
+ :access_token,
55
+ :api_endpoint,
56
+ :auto_paginate,
57
+ :client_id,
58
+ :client_secret,
59
+ :connection_options,
60
+ :default_media_type,
61
+ :middleware,
62
+ :netrc,
63
+ :netrc_file,
64
+ :per_page,
65
+ :proxy,
66
+ :user_agent,
67
+ :faraday,
68
+ :shared_swagger,
69
+ :swagger,
70
+ :raw_responses,
71
+ :web_endpoint
72
+ ]
73
+ end
74
+ end
75
+
76
+ # Set configuration options using a block
77
+ def configure
78
+ yield self
79
+ end
80
+
81
+ # Reset configuration options to default values
82
+ def reset!
83
+ LookerSDK::Configurable.keys.each do |key|
84
+ instance_variable_set(:"@#{key}", LookerSDK::Default.options[key])
85
+ end
86
+ self
87
+ end
88
+ alias setup reset!
89
+
90
+ def api_endpoint
91
+ File.join(@api_endpoint, "")
92
+ end
93
+
94
+ # Base URL for generated web URLs
95
+ #
96
+ # @return [String] Default: https://<client>.looker.com/ look TODO is this correct?
97
+ def web_endpoint
98
+ File.join(@web_endpoint, "")
99
+ end
100
+
101
+ def netrc?
102
+ !!@netrc
103
+ end
104
+
105
+ private
106
+
107
+ def options
108
+ Hash[LookerSDK::Configurable.keys.map{|key| [key, instance_variable_get(:"@#{key}")]}]
109
+ end
110
+
111
+ def fetch_client_id_and_secret(overrides = {})
112
+ opts = options.merge(overrides)
113
+ opts.values_at :client_id, :client_secret
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,148 @@
1
+ require 'looker-sdk/response/raise_error'
2
+ require 'looker-sdk/version'
3
+
4
+ module LookerSDK
5
+
6
+ # Default configuration options for {Client}
7
+ module Default
8
+
9
+ # Default API endpoint look TODO update this as needed
10
+ API_ENDPOINT = "https://localhost:19999/api/3.0/".freeze
11
+
12
+ # Default User Agent header string
13
+ USER_AGENT = "Looker Ruby Gem #{LookerSDK::VERSION}".freeze
14
+
15
+ # Default media type
16
+ MEDIA_TYPE = "application/json"
17
+
18
+ # Default WEB endpoint
19
+ WEB_ENDPOINT = "https://localhost:9999".freeze # look TODO update this
20
+
21
+ # In Faraday 0.9, Faraday::Builder was renamed to Faraday::RackBuilder
22
+ RACK_BUILDER_CLASS = defined?(Faraday::RackBuilder) ? Faraday::RackBuilder : Faraday::Builder
23
+
24
+ # Default Faraday middleware stack
25
+ MIDDLEWARE = RACK_BUILDER_CLASS.new do |builder|
26
+ builder.use LookerSDK::Response::RaiseError
27
+ builder.adapter Faraday.default_adapter
28
+ end
29
+
30
+ class << self
31
+
32
+ # Configuration options
33
+ # @return [Hash]
34
+ def options
35
+ Hash[LookerSDK::Configurable.keys.map{|key| [key, send(key)]}]
36
+ end
37
+
38
+ # Default access token from ENV
39
+ # @return [String]
40
+ def access_token
41
+ ENV['LOOKER_ACCESS_TOKEN']
42
+ end
43
+
44
+ # Default API endpoint from ENV or {API_ENDPOINT}
45
+ # @return [String]
46
+ def api_endpoint
47
+ ENV['LOOKER_API_ENDPOINT'] || API_ENDPOINT
48
+ end
49
+
50
+ # Default pagination preference from ENV
51
+ # @return [String]
52
+ def auto_paginate
53
+ ENV['LOOKER_AUTO_PAGINATE']
54
+ end
55
+
56
+ # Default OAuth app key from ENV
57
+ # @return [String]
58
+ def client_id
59
+ ENV['LOOKER_CLIENT_ID']
60
+ end
61
+
62
+ # Default OAuth app secret from ENV
63
+ # @return [String]
64
+ def client_secret
65
+ ENV['LOOKER_SECRET']
66
+ end
67
+
68
+ # Default options for Faraday::Connection
69
+ # @return [Hash]
70
+ def connection_options
71
+ {
72
+ :headers => {
73
+ :accept => default_media_type,
74
+ :user_agent => user_agent
75
+ }
76
+ }
77
+ end
78
+
79
+ # Default media type from ENV or {MEDIA_TYPE}
80
+ # @return [String]
81
+ def default_media_type
82
+ ENV['LOOKER_DEFAULT_MEDIA_TYPE'] || MEDIA_TYPE
83
+ end
84
+
85
+ # Default middleware stack for Faraday::Connection
86
+ # from {MIDDLEWARE}
87
+ # @return [String]
88
+ def middleware
89
+ MIDDLEWARE
90
+ end
91
+
92
+ def faraday
93
+ nil
94
+ end
95
+
96
+ def swagger
97
+ nil
98
+ end
99
+
100
+ def shared_swagger
101
+ false
102
+ end
103
+
104
+ def raw_responses
105
+ false
106
+ end
107
+
108
+ # Default pagination page size from ENV
109
+ # @return [Fixnum] Page size
110
+ def per_page
111
+ page_size = ENV['LOOKER_PER_PAGE']
112
+
113
+ page_size.to_i if page_size
114
+ end
115
+
116
+ # Default proxy server URI for Faraday connection from ENV
117
+ # @return [String]
118
+ def proxy
119
+ ENV['LOOKER_PROXY']
120
+ end
121
+
122
+ # Default User-Agent header string from ENV or {USER_AGENT}
123
+ # @return [String]
124
+ def user_agent
125
+ ENV['LOOKER_USER_AGENT'] || USER_AGENT
126
+ end
127
+
128
+ # Default web endpoint from ENV or {WEB_ENDPOINT}
129
+ # @return [String]
130
+ def web_endpoint
131
+ ENV['LOOKER_WEB_ENDPOINT'] || WEB_ENDPOINT
132
+ end
133
+
134
+ # Default behavior for reading .netrc file
135
+ # @return [Boolean]
136
+ def netrc
137
+ ENV['LOOKER_NETRC'] || false
138
+ end
139
+
140
+ # Default path for .netrc file
141
+ # @return [String]
142
+ def netrc_file
143
+ ENV['LOOKER_NETRC_FILE'] || File.join(ENV['HOME'].to_s, '.netrc')
144
+ end
145
+
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,235 @@
1
+ module LookerSDK
2
+ class Error < StandardError
3
+
4
+ # Returns the appropriate LookerSDK::Error sublcass based
5
+ # on status and response message
6
+ #
7
+ # @param [Hash] response HTTP response
8
+ # @return [LookerSDK::Error]
9
+ def self.from_response(response)
10
+ status = response[:status].to_i
11
+ body = response[:body].to_s
12
+ headers = response[:response_headers]
13
+
14
+ if klass = case status
15
+ when 400 then LookerSDK::BadRequest
16
+ when 401 then error_for_401(headers)
17
+ when 403 then error_for_403(body)
18
+ when 404 then LookerSDK::NotFound
19
+ when 405 then LookerSDK::MethodNotAllowed
20
+ when 406 then LookerSDK::NotAcceptable
21
+ when 409 then LookerSDK::Conflict
22
+ when 415 then LookerSDK::UnsupportedMediaType
23
+ when 422 then LookerSDK::UnprocessableEntity
24
+ when 400..499 then LookerSDK::ClientError
25
+ when 500 then LookerSDK::InternalServerError
26
+ when 501 then LookerSDK::NotImplemented
27
+ when 502 then LookerSDK::BadGateway
28
+ when 503 then LookerSDK::ServiceUnavailable
29
+ when 500..599 then LookerSDK::ServerError
30
+ end
31
+ klass.new(response)
32
+ end
33
+ end
34
+
35
+ def initialize(response=nil)
36
+ @response = response
37
+ super(build_error_message)
38
+ end
39
+
40
+ # Documentation URL returned by the API for some errors
41
+ #
42
+ # @return [String]
43
+ def documentation_url
44
+ data[:documentation_url] if data.is_a? Hash
45
+ end
46
+
47
+ # Message string returned by the API for some errors
48
+ #
49
+ # @return [String]
50
+ def message
51
+ response_message
52
+ end
53
+
54
+ # Returns most appropriate error for 401 HTTP status code
55
+ # @private
56
+ def self.error_for_401(headers)
57
+ if LookerSDK::OneTimePasswordRequired.required_header(headers)
58
+ LookerSDK::OneTimePasswordRequired
59
+ else
60
+ LookerSDK::Unauthorized
61
+ end
62
+ end
63
+
64
+ # Returns most appropriate error for 403 HTTP status code
65
+ # @private
66
+ def self.error_for_403(body)
67
+ if body =~ /rate limit exceeded/i
68
+ LookerSDK::TooManyRequests
69
+ elsif body =~ /login attempts exceeded/i
70
+ LookerSDK::TooManyLoginAttempts
71
+ else
72
+ LookerSDK::Forbidden
73
+ end
74
+ end
75
+
76
+ # Array of validation errors
77
+ # @return [Array<Hash>] Error info
78
+ def errors
79
+ if data && data.is_a?(Hash)
80
+ data[:errors] || []
81
+ else
82
+ []
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ def data
89
+ @data ||=
90
+ if (body = @response[:body]) && !body.empty?
91
+ if body.is_a?(String) &&
92
+ @response[:response_headers] &&
93
+ @response[:response_headers][:content_type] =~ /json/
94
+
95
+ Sawyer::Agent.serializer.decode(body)
96
+ else
97
+ body
98
+ end
99
+ else
100
+ nil
101
+ end
102
+ end
103
+
104
+ def response_message
105
+ case data
106
+ when Hash
107
+ data[:message]
108
+ when String
109
+ data
110
+ end
111
+ end
112
+
113
+ def response_error
114
+ "Error: #{data[:error]}" if data.is_a?(Hash) && data[:error]
115
+ end
116
+
117
+ def response_error_summary
118
+ return nil unless data.is_a?(Hash) && !Array(data[:errors]).empty?
119
+
120
+ summary = "\nError summary:\n"
121
+ summary << data[:errors].map do |hash|
122
+ hash.map { |k,v| " #{k}: #{v}" }
123
+ end.join("\n")
124
+
125
+ summary
126
+ end
127
+
128
+ def build_error_message
129
+ return nil if @response.nil?
130
+
131
+ message = "#{@response[:method].to_s.upcase} "
132
+ message << redact_url(@response[:url].to_s) + ": "
133
+ message << "#{@response[:status]} - "
134
+ message << "#{response_message}" unless response_message.nil?
135
+ message << "#{response_error}" unless response_error.nil?
136
+ message << "#{response_error_summary}" unless response_error_summary.nil?
137
+ message << " // See: #{documentation_url}" unless documentation_url.nil?
138
+ message
139
+ end
140
+
141
+ def redact_url(url_string)
142
+ %w[client_secret access_token].each do |token|
143
+ url_string.gsub!(/#{token}=\S+/, "#{token}=(redacted)") if url_string.include? token
144
+ end
145
+ url_string
146
+ end
147
+ end
148
+
149
+ # Raised on errors in the 400-499 range
150
+ class ClientError < Error; end
151
+
152
+ # Raised when API returns a 400 HTTP status code
153
+ class BadRequest < ClientError; end
154
+
155
+ # Raised when API returns a 401 HTTP status code
156
+ class Unauthorized < ClientError; end
157
+
158
+ # Raised when API returns a 401 HTTP status code
159
+ # and headers include "X-Looker-OTP" look TODO do we want to support this?
160
+ class OneTimePasswordRequired < ClientError
161
+ #@private
162
+ OTP_DELIVERY_PATTERN = /required; (\w+)/i
163
+
164
+ #@private
165
+ def self.required_header(headers)
166
+ OTP_DELIVERY_PATTERN.match headers['X-Looker-OTP'].to_s
167
+ end
168
+
169
+ # Delivery method for the user's OTP
170
+ #
171
+ # @return [String]
172
+ def password_delivery
173
+ @password_delivery ||= delivery_method_from_header
174
+ end
175
+
176
+ private
177
+
178
+ def delivery_method_from_header
179
+ if match = self.class.required_header(@response[:response_headers])
180
+ match[1]
181
+ end
182
+ end
183
+ end
184
+
185
+ # Raised when Looker returns a 403 HTTP status code
186
+ class Forbidden < ClientError; end
187
+
188
+ # Raised when Looker returns a 403 HTTP status code
189
+ # and body matches 'rate limit exceeded'
190
+ class TooManyRequests < Forbidden; end
191
+
192
+ # Raised when Looker returns a 403 HTTP status code
193
+ # and body matches 'login attempts exceeded'
194
+ class TooManyLoginAttempts < Forbidden; end
195
+
196
+ # Raised when Looker returns a 404 HTTP status code
197
+ class NotFound < ClientError; end
198
+
199
+ # Raised when Looker returns a 405 HTTP status code
200
+ class MethodNotAllowed < ClientError; end
201
+
202
+ # Raised when Looker returns a 406 HTTP status code
203
+ class NotAcceptable < ClientError; end
204
+
205
+ # Raised when Looker returns a 409 HTTP status code
206
+ class Conflict < ClientError; end
207
+
208
+ # Raised when Looker returns a 414 HTTP status code
209
+ class UnsupportedMediaType < ClientError; end
210
+
211
+ # Raised when Looker returns a 422 HTTP status code
212
+ class UnprocessableEntity < ClientError; end
213
+
214
+ # Raised on errors in the 500-599 range
215
+ class ServerError < Error; end
216
+
217
+ # Raised when Looker returns a 500 HTTP status code
218
+ class InternalServerError < ServerError; end
219
+
220
+ # Raised when Looker returns a 501 HTTP status code
221
+ class NotImplemented < ServerError; end
222
+
223
+ # Raised when Looker returns a 502 HTTP status code
224
+ class BadGateway < ServerError; end
225
+
226
+ # Raised when Looker returns a 503 HTTP status code
227
+ class ServiceUnavailable < ServerError; end
228
+
229
+ # Raised when client fails to provide valid Content-Type
230
+ class MissingContentType < ArgumentError; end
231
+
232
+ # Raised when a method requires an application client_id
233
+ # and secret but none is provided
234
+ class ApplicationCredentialsRequired < StandardError; end
235
+ end