looker-sdk 0.0.5

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 (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