looker-sdk 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +52 -0
- data/.ruby-gemset +1 -0
- data/.travis.yml +16 -0
- data/Gemfile +22 -0
- data/LICENSE +21 -0
- data/Rakefile +37 -0
- data/authentication.md +104 -0
- data/examples/add_delete_users.rb +94 -0
- data/examples/change_credentials_email_address_for_users.rb +23 -0
- data/examples/create_credentials_email_for_users.rb +19 -0
- data/examples/delete_all_user_sessions.rb +15 -0
- data/examples/delete_credentials_google_for_users.rb +19 -0
- data/examples/generate_password_reset_tokens_for_users.rb +19 -0
- data/examples/ldap_roles_test.rb +50 -0
- data/examples/me.rb +3 -0
- data/examples/refresh_user_notification_addresses.rb +10 -0
- data/examples/roles_and_users_with_permission.rb +22 -0
- data/examples/sdk_setup.rb +21 -0
- data/examples/streaming_downloads.rb +20 -0
- data/examples/users_with_credentials_email.rb +6 -0
- data/examples/users_with_credentials_google.rb +8 -0
- data/examples/users_with_credentials_google_without_credentials_email.rb +6 -0
- data/lib/looker-sdk.rb +32 -0
- data/lib/looker-sdk/authentication.rb +104 -0
- data/lib/looker-sdk/client.rb +445 -0
- data/lib/looker-sdk/client/dynamic.rb +107 -0
- data/lib/looker-sdk/configurable.rb +116 -0
- data/lib/looker-sdk/default.rb +148 -0
- data/lib/looker-sdk/error.rb +235 -0
- data/lib/looker-sdk/rate_limit.rb +33 -0
- data/lib/looker-sdk/response/raise_error.rb +20 -0
- data/lib/looker-sdk/sawyer_patch.rb +33 -0
- data/lib/looker-sdk/version.rb +7 -0
- data/looker-sdk.gemspec +27 -0
- data/readme.md +117 -0
- data/shell/.gitignore +41 -0
- data/shell/Gemfile +6 -0
- data/shell/readme.md +18 -0
- data/shell/shell.rb +37 -0
- data/streaming.md +59 -0
- data/test/helper.rb +46 -0
- data/test/looker/swagger.json +1998 -0
- data/test/looker/test_client.rb +258 -0
- data/test/looker/test_dynamic_client.rb +158 -0
- data/test/looker/test_dynamic_client_agent.rb +131 -0
- data/test/looker/user.json +1 -0
- metadata +107 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
require './sdk_setup'
|
2
|
+
|
3
|
+
$stdin.each_line do |line|
|
4
|
+
line.chomp!
|
5
|
+
|
6
|
+
id = line.split(',', 2).map(&:strip).first
|
7
|
+
|
8
|
+
begin
|
9
|
+
user = sdk.user(id)
|
10
|
+
if user.credentials_email
|
11
|
+
token = sdk.create_user_credentials_email_password_reset(id)
|
12
|
+
puts "#{token.email},#{token.password_reset_url}"
|
13
|
+
else
|
14
|
+
puts "Error: User with id '#{id}' Does not have credentials_email"
|
15
|
+
end
|
16
|
+
rescue LookerSDK::NotFound
|
17
|
+
puts "Error: User with id '#{id}' Not found"
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require './sdk_setup'
|
2
|
+
|
3
|
+
############################################################################################
|
4
|
+
# simulate a list read from file
|
5
|
+
|
6
|
+
user_list = <<-ENDMARK
|
7
|
+
|
8
|
+
mward
|
9
|
+
mwhite
|
10
|
+
missing
|
11
|
+
|
12
|
+
ENDMARK
|
13
|
+
|
14
|
+
############################################################################################
|
15
|
+
# helpers
|
16
|
+
|
17
|
+
def ldap_config
|
18
|
+
@ldap_config ||= sdk.ldap_config.to_attrs
|
19
|
+
end
|
20
|
+
|
21
|
+
def groups_map_for_config
|
22
|
+
@groups_map_for_config ||= ldap_config[:groups].map do |group|
|
23
|
+
{:name => group[:name], :role_ids => group[:roles].map{|role| role[:id]}}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_ldap_user(user)
|
28
|
+
params = ldap_config.merge({:groups_with_role_ids => groups_map_for_config, :test_ldap_user => user})
|
29
|
+
sdk.test_ldap_config_user_info(params)
|
30
|
+
end
|
31
|
+
|
32
|
+
############################################################################################
|
33
|
+
# process the list and puts results
|
34
|
+
|
35
|
+
user_list.each_line do |user|
|
36
|
+
user.strip!
|
37
|
+
next if user.empty?
|
38
|
+
|
39
|
+
puts "'#{user}' ..."
|
40
|
+
|
41
|
+
result = test_ldap_user(user).to_attrs
|
42
|
+
if result[:status] == 'success'
|
43
|
+
puts "Success"
|
44
|
+
puts result[:user]
|
45
|
+
else
|
46
|
+
puts "FAILURE"
|
47
|
+
puts result
|
48
|
+
end
|
49
|
+
puts
|
50
|
+
end
|
data/examples/me.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require './sdk_setup'
|
2
|
+
|
3
|
+
sdk.all_users(:fields => 'id,email').each do |user|
|
4
|
+
new_user = sdk.update_user(user.id, {})
|
5
|
+
if user.email == new_user.email
|
6
|
+
puts "No Change for #{user.id}"
|
7
|
+
else
|
8
|
+
puts "Refreshed #{user.id}. Old email '#{user.email}'. Refreshed email: '#{new_user.email}'."
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require './sdk_setup'
|
2
|
+
|
3
|
+
|
4
|
+
while true do
|
5
|
+
print "permission [,model]? "
|
6
|
+
permission_name, model_name = gets.chomp.split(',')
|
7
|
+
|
8
|
+
break if permission_name.empty?
|
9
|
+
|
10
|
+
roles = sdk.all_roles.select do |role|
|
11
|
+
(role.permission_set.all_access || role.permission_set.permissions.join(',').include?(permission_name)) &&
|
12
|
+
(model_name.nil? || role.model_set.all_access || role.model_set.models.join(',').include?(model_name))
|
13
|
+
end
|
14
|
+
|
15
|
+
puts "Roles: #{roles.map(&:name).join(', ')}"
|
16
|
+
|
17
|
+
role_ids = roles.map(&:id)
|
18
|
+
users = sdk.all_users.select {|user| (user.role_ids & role_ids).any?}
|
19
|
+
user_names = users.map{|u| "#{u.id}#{" ("+u.display_name+")" if u.display_name}"}.join(', ')
|
20
|
+
|
21
|
+
puts "Users: #{user_names}"
|
22
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
|
4
|
+
require 'looker-sdk'
|
5
|
+
|
6
|
+
# common file used by various examples to setup and init sdk
|
7
|
+
|
8
|
+
def sdk
|
9
|
+
@sdk ||= LookerSDK::Client.new(
|
10
|
+
:netrc => true,
|
11
|
+
:netrc_file => "./.netrc",
|
12
|
+
|
13
|
+
# use my local looker with self-signed cert
|
14
|
+
:connection_options => {:ssl => {:verify => false}},
|
15
|
+
:api_endpoint => "https://localhost:19999/api/3.0",
|
16
|
+
|
17
|
+
# use a real looker the way you are supposed to!
|
18
|
+
# :connection_options => {:ssl => {:verify => true}},
|
19
|
+
# :api_endpoint => "https://mycoolcompany.looker.com:19999/api/3.0",
|
20
|
+
)
|
21
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require './sdk_setup'
|
2
|
+
|
3
|
+
# This snippet shows how to download sdk responses using http streaming.
|
4
|
+
# Streaming processes the download in chunks which you can write
|
5
|
+
# to file or perform other processing on without having to wait for the entire
|
6
|
+
# response to download first. Streaming can be very memory efficient
|
7
|
+
# for handling large result sets compared to just downloading the whole thing into
|
8
|
+
# a Ruby object in memory.
|
9
|
+
|
10
|
+
def run_look_to_file(look_id, filename, format, opts = {})
|
11
|
+
File.open(filename, 'w') do |file|
|
12
|
+
sdk.run_look(look_id, format, opts) do |data, progress|
|
13
|
+
file.write(data)
|
14
|
+
puts "Wrote #{data.length} bytes of #{progress.length} total"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Replace the look id (38) with the id of your actual look
|
20
|
+
run_look_to_file(38, 'out.csv', 'csv', limit: 10000)
|
@@ -0,0 +1,8 @@
|
|
1
|
+
# sdk.all_users(:fields => 'id,credentials_google').select{|u| u.credentials_google}.map{|u| u.id}
|
2
|
+
|
3
|
+
require './sdk_setup'
|
4
|
+
|
5
|
+
users = sdk.all_users(:fields => 'id, credentials_google').
|
6
|
+
select {|u| u.credentials_google}
|
7
|
+
|
8
|
+
users.each {|u| puts "#{u.id},#{u.credentials_google.email}"}
|
@@ -0,0 +1,6 @@
|
|
1
|
+
require './sdk_setup'
|
2
|
+
|
3
|
+
users = sdk.all_users(:fields => 'id, is_disabled, credentials_email, credentials_google').
|
4
|
+
select {|u| !u.is_diabled && u.credentials_google && u.credentials_google.email && !u.credentials_email}
|
5
|
+
|
6
|
+
users.each {|u| puts "#{u.id},#{u.credentials_google.email}"}
|
data/lib/looker-sdk.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'looker-sdk/client'
|
2
|
+
require 'looker-sdk/default'
|
3
|
+
|
4
|
+
module LookerSDK
|
5
|
+
|
6
|
+
class << self
|
7
|
+
include LookerSDK::Configurable
|
8
|
+
|
9
|
+
# API client based on configured options {Configurable}
|
10
|
+
#
|
11
|
+
# @return [LookerSDK::Client] API wrapper
|
12
|
+
def client
|
13
|
+
@client = LookerSDK::Client.new(options) unless defined?(@client) && @client.same_options?(options)
|
14
|
+
@client
|
15
|
+
end
|
16
|
+
|
17
|
+
# @private
|
18
|
+
def respond_to_missing?(method_name, include_private=false); client.respond_to?(method_name, include_private); end if RUBY_VERSION >= "1.9"
|
19
|
+
# @private
|
20
|
+
def respond_to?(method_name, include_private=false); client.respond_to?(method_name, include_private) || super; end if RUBY_VERSION < "1.9"
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def method_missing(method_name, *args, &block)
|
25
|
+
return super unless client.respond_to?(method_name)
|
26
|
+
client.send(method_name, *args, &block)
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
LookerSDK.setup
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module LookerSDK
|
2
|
+
|
3
|
+
# Authentication methods for {LookerSDK::Client}
|
4
|
+
module Authentication
|
5
|
+
|
6
|
+
attr_accessor :access_token_type, :access_token_expires_at
|
7
|
+
|
8
|
+
# This is called automatically by 'request'
|
9
|
+
def ensure_logged_in
|
10
|
+
authenticate unless token_authenticated? || @skip_authenticate
|
11
|
+
end
|
12
|
+
|
13
|
+
def without_authentication
|
14
|
+
begin
|
15
|
+
old_skip = @skip_authenticate || false
|
16
|
+
@skip_authenticate = true
|
17
|
+
yield
|
18
|
+
ensure
|
19
|
+
@skip_authenticate = old_skip
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Authenticate to the server and get an access_token for use in future calls.
|
24
|
+
|
25
|
+
def authenticate
|
26
|
+
raise "client_id and client_secret required" unless application_credentials?
|
27
|
+
|
28
|
+
set_access_token_from_params(nil)
|
29
|
+
without_authentication do
|
30
|
+
post('/login', {}, :query => application_credentials)
|
31
|
+
raise "login failure #{last_response.status}" unless last_response.status == 200
|
32
|
+
set_access_token_from_params(last_response.data)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def set_access_token_from_params(params)
|
37
|
+
reset_agent
|
38
|
+
if params
|
39
|
+
@access_token = params[:access_token]
|
40
|
+
@access_token_type = params[:token_type]
|
41
|
+
@access_token_expires_at = Time.now + params[:expires_in]
|
42
|
+
else
|
43
|
+
@access_token = @access_token_type = @access_token_expires_at = nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def logout
|
48
|
+
without_authentication do
|
49
|
+
result = !!@access_token && ((delete('/logout') ; delete_succeeded?) rescue false)
|
50
|
+
set_access_token_from_params(nil)
|
51
|
+
result
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
# Indicates if the client has OAuth Application
|
57
|
+
# client_id and client_secret credentials
|
58
|
+
#
|
59
|
+
# @see look TODO docs link
|
60
|
+
# @return Boolean
|
61
|
+
def application_credentials?
|
62
|
+
!!application_credentials
|
63
|
+
end
|
64
|
+
|
65
|
+
# Indicates if the client has an OAuth
|
66
|
+
# access token
|
67
|
+
#
|
68
|
+
# @see look TODO docs link
|
69
|
+
# @return [Boolean]
|
70
|
+
def token_authenticated?
|
71
|
+
!!(@access_token && (@access_token_expires_at.nil? || @access_token_expires_at > Time.now))
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def application_credentials
|
77
|
+
if @client_id && @client_secret
|
78
|
+
{
|
79
|
+
:client_id => @client_id,
|
80
|
+
:client_secret => @client_secret
|
81
|
+
}
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def load_credentials_from_netrc
|
86
|
+
return unless netrc?
|
87
|
+
|
88
|
+
require 'netrc'
|
89
|
+
info = Netrc.read File.expand_path(netrc_file)
|
90
|
+
netrc_host = URI.parse(api_endpoint).host
|
91
|
+
creds = info[netrc_host]
|
92
|
+
if creds.nil?
|
93
|
+
# creds will be nil if there is no netrc for this end point
|
94
|
+
looker_warn "Error loading credentials from netrc file for #{api_endpoint}"
|
95
|
+
else
|
96
|
+
self.client_id = creds[0]
|
97
|
+
self.client_secret = creds[1]
|
98
|
+
end
|
99
|
+
rescue LoadError
|
100
|
+
looker_warn "Please install netrc gem for .netrc support"
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,445 @@
|
|
1
|
+
require 'sawyer'
|
2
|
+
require 'ostruct'
|
3
|
+
require 'looker-sdk/sawyer_patch'
|
4
|
+
require 'looker-sdk/configurable'
|
5
|
+
require 'looker-sdk/authentication'
|
6
|
+
require 'looker-sdk/rate_limit'
|
7
|
+
require 'looker-sdk/client/dynamic'
|
8
|
+
|
9
|
+
module LookerSDK
|
10
|
+
|
11
|
+
# Client for the LookerSDK API
|
12
|
+
#
|
13
|
+
# @see look TODO docs link
|
14
|
+
class Client
|
15
|
+
|
16
|
+
include LookerSDK::Authentication
|
17
|
+
include LookerSDK::Configurable
|
18
|
+
include LookerSDK::Client::Dynamic
|
19
|
+
|
20
|
+
# Header keys that can be passed in options hash to {#get},{#head}
|
21
|
+
CONVENIENCE_HEADERS = Set.new([:accept, :content_type])
|
22
|
+
|
23
|
+
def initialize(opts = {})
|
24
|
+
# Use options passed in, but fall back to module defaults
|
25
|
+
LookerSDK::Configurable.keys.each do |key|
|
26
|
+
instance_variable_set(:"@#{key}", opts[key] || LookerSDK.instance_variable_get(:"@#{key}"))
|
27
|
+
end
|
28
|
+
|
29
|
+
# allow caller to do configuration in a block before we load swagger and become dynamic
|
30
|
+
yield self if block_given?
|
31
|
+
|
32
|
+
# Save the original state of the options because live variables received later like access_token and
|
33
|
+
# client_id appear as if they are options and confuse the automatic client generation in LookerSDK#client
|
34
|
+
@original_options = options.dup
|
35
|
+
|
36
|
+
load_credentials_from_netrc unless application_credentials?
|
37
|
+
load_swagger
|
38
|
+
self.dynamic = true
|
39
|
+
end
|
40
|
+
|
41
|
+
# Compares client options to a Hash of requested options
|
42
|
+
#
|
43
|
+
# @param opts [Hash] Options to compare with current client options
|
44
|
+
# @return [Boolean]
|
45
|
+
def same_options?(opts)
|
46
|
+
opts.hash == @original_options.hash
|
47
|
+
end
|
48
|
+
|
49
|
+
# Text representation of the client, masking tokens and passwords
|
50
|
+
#
|
51
|
+
# @return [String]
|
52
|
+
def inspect
|
53
|
+
inspected = super
|
54
|
+
|
55
|
+
# Only show last 4 of token, secret
|
56
|
+
[@access_token, @client_secret].compact.each do |str|
|
57
|
+
len = [str.size - 4, 0].max
|
58
|
+
inspected = inspected.gsub! str, "#{'*'*len}#{str[len..-1]}"
|
59
|
+
end
|
60
|
+
|
61
|
+
inspected
|
62
|
+
end
|
63
|
+
|
64
|
+
# Make a HTTP GET request
|
65
|
+
#
|
66
|
+
# @param url [String] The path, relative to {#api_endpoint}
|
67
|
+
# @param options [Hash] Query and header params for request
|
68
|
+
# @param &block [Block] Block to be called with |response, chunk| for each chunk of the body from
|
69
|
+
# the server. The block must return true to continue, or false to abort streaming.
|
70
|
+
# @return [Sawyer::Resource]
|
71
|
+
def get(url, options = {}, &block)
|
72
|
+
request :get, url, nil, parse_query_and_convenience_headers(options), &block
|
73
|
+
end
|
74
|
+
|
75
|
+
# Make a HTTP POST request
|
76
|
+
#
|
77
|
+
# @param url [String] The path, relative to {#api_endpoint}
|
78
|
+
# @param data [String|Array|Hash] Body and optionally header params for request
|
79
|
+
# @param options [Hash] Optional header params for request
|
80
|
+
# @param &block [Block] Block to be called with |response, chunk| for each chunk of the body from
|
81
|
+
# the server. The block must return true to continue, or false to abort streaming.
|
82
|
+
# @return [Sawyer::Resource]
|
83
|
+
def post(url, data = {}, options = {}, &block)
|
84
|
+
request :post, url, data, parse_query_and_convenience_headers(options), &block
|
85
|
+
end
|
86
|
+
|
87
|
+
# Make a HTTP PUT request
|
88
|
+
#
|
89
|
+
# @param url [String] The path, relative to {#api_endpoint}
|
90
|
+
# @param data [String|Array|Hash] Body and optionally header params for request
|
91
|
+
# @param options [Hash] Optional header params for request
|
92
|
+
# @param &block [Block] Block to be called with |response, chunk| for each chunk of the body from
|
93
|
+
# the server. The block must return true to continue, or false to abort streaming.
|
94
|
+
# @return [Sawyer::Resource]
|
95
|
+
def put(url, data = {}, options = {}, &block)
|
96
|
+
request :put, url, data, parse_query_and_convenience_headers(options), &block
|
97
|
+
end
|
98
|
+
|
99
|
+
# Make a HTTP PATCH request
|
100
|
+
#
|
101
|
+
# @param url [String] The path, relative to {#api_endpoint}
|
102
|
+
# @param data [String|Array|Hash] Body and optionally header params for request
|
103
|
+
# @param options [Hash] Optional header params for request
|
104
|
+
# @param &block [Block] Block to be called with |response, chunk| for each chunk of the body from
|
105
|
+
# the server. The block must return true to continue, or false to abort streaming.
|
106
|
+
# @return [Sawyer::Resource]
|
107
|
+
def patch(url, data = {}, options = {}, &block)
|
108
|
+
request :patch, url, data, parse_query_and_convenience_headers(options), &block
|
109
|
+
end
|
110
|
+
|
111
|
+
# Make a HTTP DELETE request
|
112
|
+
#
|
113
|
+
# @param url [String] The path, relative to {#api_endpoint}
|
114
|
+
# @param options [Hash] Query and header params for request
|
115
|
+
# @return [Sawyer::Resource]
|
116
|
+
def delete(url, options = {}, &block)
|
117
|
+
request :delete, url, nil, parse_query_and_convenience_headers(options)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Make a HTTP HEAD request
|
121
|
+
#
|
122
|
+
# @param url [String] The path, relative to {#api_endpoint}
|
123
|
+
# @param options [Hash] Query and header params for request
|
124
|
+
# @return [Sawyer::Resource]
|
125
|
+
def head(url, options = {}, &block)
|
126
|
+
request :head, url, nil, parse_query_and_convenience_headers(options)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Make one or more HTTP GET requests, optionally fetching
|
130
|
+
# the next page of results from URL in Link response header based
|
131
|
+
# on value in {#auto_paginate}.
|
132
|
+
#
|
133
|
+
# @param url [String] The path, relative to {#api_endpoint}
|
134
|
+
# @param options [Hash] Query and header params for request
|
135
|
+
# @param block [Block] Block to perform the data concatination of the
|
136
|
+
# multiple requests. The block is called with two parameters, the first
|
137
|
+
# contains the contents of the requests so far and the second parameter
|
138
|
+
# contains the latest response.
|
139
|
+
# @return [Sawyer::Resource]
|
140
|
+
def paginate(url, options = {}, &block)
|
141
|
+
opts = parse_query_and_convenience_headers(options)
|
142
|
+
if @auto_paginate || @per_page
|
143
|
+
opts[:query][:per_page] ||= @per_page || (@auto_paginate ? 100 : nil)
|
144
|
+
end
|
145
|
+
|
146
|
+
data = request(:get, url, nil, opts)
|
147
|
+
|
148
|
+
if @auto_paginate
|
149
|
+
while @last_response.rels[:next] && rate_limit.remaining > 0
|
150
|
+
@last_response = @last_response.rels[:next].get
|
151
|
+
if block_given?
|
152
|
+
yield(data, @last_response)
|
153
|
+
else
|
154
|
+
data.concat(@last_response.data) if @last_response.data.is_a?(Array)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
end
|
159
|
+
|
160
|
+
data
|
161
|
+
end
|
162
|
+
|
163
|
+
# Hypermedia agent for the LookerSDK API (with specific options)
|
164
|
+
#
|
165
|
+
# @return [Sawyer::Agent]
|
166
|
+
def make_agent(options = nil)
|
167
|
+
options ||= sawyer_options
|
168
|
+
Sawyer::Agent.new(api_endpoint, options) do |http|
|
169
|
+
http.headers[:accept] = default_media_type
|
170
|
+
http.headers[:user_agent] = user_agent
|
171
|
+
http.authorization('token', @access_token) if token_authenticated?
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# Cached Hypermedia agent for the LookerSDK API (with default options)
|
176
|
+
#
|
177
|
+
# @return [Sawyer::Agent]
|
178
|
+
def agent
|
179
|
+
@agent ||= make_agent
|
180
|
+
end
|
181
|
+
|
182
|
+
# Fetch the root resource for the API
|
183
|
+
#
|
184
|
+
# @return [Sawyer::Resource]
|
185
|
+
def root
|
186
|
+
get URI(api_endpoint).path.sub(/\/$/,'')
|
187
|
+
end
|
188
|
+
|
189
|
+
# Is the server alive (this can be called w/o authentication)
|
190
|
+
#
|
191
|
+
# @return http status code
|
192
|
+
def alive
|
193
|
+
get '/alive'
|
194
|
+
last_response.status
|
195
|
+
end
|
196
|
+
|
197
|
+
# Response for last HTTP request
|
198
|
+
#
|
199
|
+
# @return [Sawyer::Response]
|
200
|
+
def last_response
|
201
|
+
@last_response if defined? @last_response
|
202
|
+
end
|
203
|
+
|
204
|
+
# Response for last HTTP request
|
205
|
+
#
|
206
|
+
# @return [StandardError]
|
207
|
+
def last_error
|
208
|
+
@last_error if defined? @last_error
|
209
|
+
end
|
210
|
+
|
211
|
+
# Set OAuth access token for authentication
|
212
|
+
#
|
213
|
+
# @param value [String] Looker OAuth access token
|
214
|
+
def access_token=(value)
|
215
|
+
reset_agent
|
216
|
+
@access_token = value
|
217
|
+
end
|
218
|
+
|
219
|
+
# Set OAuth app client_id
|
220
|
+
#
|
221
|
+
# @param value [String] Looker OAuth app client_id
|
222
|
+
def client_id=(value)
|
223
|
+
reset_agent
|
224
|
+
@client_id = value
|
225
|
+
end
|
226
|
+
|
227
|
+
# Set OAuth app client_secret
|
228
|
+
#
|
229
|
+
# @param value [String] Looker OAuth app client_secret
|
230
|
+
def client_secret=(value)
|
231
|
+
reset_agent
|
232
|
+
@client_secret = value
|
233
|
+
end
|
234
|
+
|
235
|
+
# Wrapper around Kernel#warn to print warnings unless
|
236
|
+
# LOOKER_SILENT is set to true.
|
237
|
+
#
|
238
|
+
# @return [nil]
|
239
|
+
def looker_warn(*message)
|
240
|
+
unless ENV['LOOKER_SILENT']
|
241
|
+
warn message
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
private
|
246
|
+
|
247
|
+
def reset_agent
|
248
|
+
@agent = nil
|
249
|
+
end
|
250
|
+
|
251
|
+
def request(method, path, data, options, &block)
|
252
|
+
ensure_logged_in
|
253
|
+
begin
|
254
|
+
@last_response = @last_error = nil
|
255
|
+
return stream_request(method, path, data, options, &block) if block_given?
|
256
|
+
@last_response = response = agent.call(method, URI::Parser.new.escape(path.to_s), data, options)
|
257
|
+
@raw_responses ? response : response.data
|
258
|
+
rescue StandardError => e
|
259
|
+
@last_error = e
|
260
|
+
raise
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def stream_request(method, path, data, options, &block)
|
265
|
+
conn_opts = faraday_options(:builder => StreamingClient.new(self, &block))
|
266
|
+
agent = make_agent(sawyer_options(:faraday => Faraday.new(conn_opts)))
|
267
|
+
@last_response = agent.call(method, URI::Parser.new.escape(path.to_s), data, options)
|
268
|
+
end
|
269
|
+
|
270
|
+
# Since Faraday currently won't do streaming for us, we use Net::HTTP. Still, we go to the trouble
|
271
|
+
# to go through the Sawyer/Faraday codepath so that we can leverage all the header and param
|
272
|
+
# processing they do in order to be as consistent as we can with the normal non-streaming codepath.
|
273
|
+
# This class replaces the default Faraday 'builder' that Faraday uses to do the actual request after
|
274
|
+
# all the setup is done.
|
275
|
+
|
276
|
+
class StreamingClient
|
277
|
+
class Progress
|
278
|
+
attr_reader :response
|
279
|
+
attr_accessor :chunks, :length
|
280
|
+
|
281
|
+
def initialize(response)
|
282
|
+
@response = response
|
283
|
+
@chunks = @length = 0
|
284
|
+
@stopped = false
|
285
|
+
end
|
286
|
+
|
287
|
+
def add_chunk(chunk)
|
288
|
+
@chunks += 1
|
289
|
+
@length += chunk.length
|
290
|
+
end
|
291
|
+
|
292
|
+
def stop
|
293
|
+
@stopped = true
|
294
|
+
end
|
295
|
+
|
296
|
+
def stopped?
|
297
|
+
@stopped
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
def initialize(client, &block)
|
302
|
+
@client, @block = client, block
|
303
|
+
end
|
304
|
+
|
305
|
+
# This is the method that faraday calls on a builder to do the actual request and build a response.
|
306
|
+
def build_response(connection, request)
|
307
|
+
full_path = connection.build_exclusive_url(request.path, request.params,
|
308
|
+
request.options.params_encoder).to_s
|
309
|
+
uri = URI(full_path)
|
310
|
+
path_with_query = uri.query ? "#{uri.path}?#{uri.query}" : uri.path
|
311
|
+
|
312
|
+
http_request = (
|
313
|
+
case request.method
|
314
|
+
when :get then Net::HTTP::Get
|
315
|
+
when :post then Net::HTTP::Post
|
316
|
+
when :put then Net::HTTP::Put
|
317
|
+
when :patch then Net::HTTP::Patch
|
318
|
+
else raise "Stream to block not supported for '#{request.method}'"
|
319
|
+
end
|
320
|
+
).new(path_with_query, request.headers)
|
321
|
+
|
322
|
+
http_request.body = request.body
|
323
|
+
|
324
|
+
connect_opts = {
|
325
|
+
:use_ssl => !!connection.ssl,
|
326
|
+
:verify_mode => (connection.ssl.verify rescue true) ?
|
327
|
+
OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE,
|
328
|
+
}
|
329
|
+
|
330
|
+
# TODO: figure out how/if to support proxies
|
331
|
+
# TODO: figure out how to test this comprehensively
|
332
|
+
|
333
|
+
progress = nil
|
334
|
+
Net::HTTP.start(uri.host, uri.port, connect_opts) do |http|
|
335
|
+
http.open_timeout = connection.options.open_timeout rescue 30
|
336
|
+
http.read_timeout = connection.options.timeout rescue 60
|
337
|
+
|
338
|
+
http.request(http_request) do |response|
|
339
|
+
progress = Progress.new(response)
|
340
|
+
if response.code == "200"
|
341
|
+
response.read_body do |chunk|
|
342
|
+
next unless chunk.length > 0
|
343
|
+
progress.add_chunk(chunk)
|
344
|
+
@block.call(chunk, progress)
|
345
|
+
return OpenStruct.new(status:"0", headers:{}, env:nil, body:nil) if progress.stopped?
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
return OpenStruct.new(status:"500", headers:{}, env:nil, body:nil) unless progress
|
352
|
+
|
353
|
+
OpenStruct.new(status:progress.response.code, headers:progress.response, env:nil, body:nil)
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
def delete_succeeded?
|
358
|
+
!!last_response && last_response.status == 204
|
359
|
+
end
|
360
|
+
|
361
|
+
class Serializer < Sawyer::Serializer
|
362
|
+
def encode(data)
|
363
|
+
data.kind_of?(Faraday::UploadIO) ? data : super
|
364
|
+
end
|
365
|
+
|
366
|
+
# slight modification to the base class' decode_hash_value function to
|
367
|
+
# less permissive when decoding time values.
|
368
|
+
#
|
369
|
+
# See https://github.com/looker/looker-sdk-ruby/issues/53 for more details
|
370
|
+
#
|
371
|
+
# Base class function that we're overriding: https://github.com/lostisland/sawyer/blob/master/lib/sawyer/serializer.rb#L101-L121
|
372
|
+
def decode_hash_value(key, value)
|
373
|
+
if time_field?(key, value) && value.is_a?(String)
|
374
|
+
begin
|
375
|
+
Time.iso8601(value)
|
376
|
+
rescue ArgumentError
|
377
|
+
value
|
378
|
+
end
|
379
|
+
else
|
380
|
+
super
|
381
|
+
end
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
def serializer
|
386
|
+
@serializer ||= (
|
387
|
+
require 'json'
|
388
|
+
Serializer.new(JSON)
|
389
|
+
)
|
390
|
+
end
|
391
|
+
|
392
|
+
def faraday_options(options = {})
|
393
|
+
conn_opts = @connection_options.clone
|
394
|
+
builder = options[:builder] || @middleware
|
395
|
+
conn_opts[:builder] = builder if builder
|
396
|
+
conn_opts[:proxy] = @proxy if @proxy
|
397
|
+
conn_opts
|
398
|
+
end
|
399
|
+
|
400
|
+
def sawyer_options(options = {})
|
401
|
+
{
|
402
|
+
:links_parser => Sawyer::LinkParsers::Simple.new,
|
403
|
+
:serializer => serializer,
|
404
|
+
:faraday => options[:faraday] || @faraday || Faraday.new(faraday_options)
|
405
|
+
}
|
406
|
+
end
|
407
|
+
|
408
|
+
def merge_content_type_if_body(body, options = {})
|
409
|
+
if body
|
410
|
+
if body.kind_of?(Faraday::UploadIO)
|
411
|
+
length = File.new(body.local_path).size.to_s
|
412
|
+
headers = {:content_type => body.content_type, :content_length => length}.merge(options[:headers] || {})
|
413
|
+
else
|
414
|
+
headers = {:content_type => default_media_type}.merge(options[:headers] || {})
|
415
|
+
end
|
416
|
+
{:headers => headers}.merge(options)
|
417
|
+
else
|
418
|
+
options
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
def parse_query_and_convenience_headers(options)
|
423
|
+
return {} if options.nil?
|
424
|
+
raise "options is not a hash" unless options.is_a?(Hash)
|
425
|
+
return {} if options.empty?
|
426
|
+
|
427
|
+
options = options.dup
|
428
|
+
headers = options.delete(:headers) || {}
|
429
|
+
CONVENIENCE_HEADERS.each do |h|
|
430
|
+
if header = options.delete(h)
|
431
|
+
headers[h] = header
|
432
|
+
end
|
433
|
+
end
|
434
|
+
query = options.delete(:query) || {}
|
435
|
+
raise "query '#{query}' is not a hash" unless query.is_a?(Hash)
|
436
|
+
query = options.merge(query)
|
437
|
+
|
438
|
+
opts = {}
|
439
|
+
opts[:query] = query unless query.empty?
|
440
|
+
opts[:headers] = headers unless headers.empty?
|
441
|
+
|
442
|
+
opts
|
443
|
+
end
|
444
|
+
end
|
445
|
+
end
|