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