├── .ruby-gemset
├── test
├── looker
│ ├── user.json
│ ├── test_dynamic_client_agent.rb
│ ├── test_dynamic_client.rb
│ └── test_client.rb
├── fixtures
│ └── .netrc.template
└── helper.rb
├── examples
├── .netrc
├── me.rb
├── users_with_credentials_google.rb
├── users_with_credentials_email.rb
├── users_with_credentials_google_without_credentials_email.rb
├── refresh_user_notification_addresses.rb
├── users_with_credentials_embed.rb
├── delete_all_user_sessions.rb
├── delete_credentials_google_for_users.rb
├── generate_password_reset_tokens_for_users.rb
├── create_credentials_email_for_users.rb
├── sdk_setup.rb
├── change_credentials_email_address_for_users.rb
├── roles_and_users_with_permission.rb
├── streaming_downloads.rb
├── ldap_roles_test.rb
├── convert_look_to_lookless_tile.rb
└── add_delete_users.rb
├── shell
├── .netrc
├── Gemfile
├── .gitignore
├── readme.md
└── shell.rb
├── .travis.yml
├── Gemfile
├── .gitignore
├── CONTRIBUTING.md
├── looker-sdk.gemspec
├── lib
├── looker-sdk
│ ├── version.rb
│ ├── response
│ │ └── raise_error.rb
│ ├── sawyer_patch.rb
│ ├── rate_limit.rb
│ ├── authentication.rb
│ ├── configurable.rb
│ ├── default.rb
│ ├── client
│ │ └── dynamic.rb
│ ├── error.rb
│ └── client.rb
└── looker-sdk.rb
├── Rakefile
├── LICENSE.md
├── streaming.md
├── Makefile
├── CODE_OF_CONDUCT.md
├── authentication.md
└── readme.md
/.ruby-gemset:
--------------------------------------------------------------------------------
1 | looker-sdk-ruby
2 |
--------------------------------------------------------------------------------
/test/looker/user.json:
--------------------------------------------------------------------------------
1 | {"first_name" : "Joe", "last_name" : "User"}
2 |
--------------------------------------------------------------------------------
/examples/.netrc:
--------------------------------------------------------------------------------
1 | machine localhost login xyYBxzSPYYP29wQJ3Q5k password VY5sj6z5qjxZsvdvf8yNvKcQ
--------------------------------------------------------------------------------
/shell/.netrc:
--------------------------------------------------------------------------------
1 | machine localhost
2 | login xyYBxzSPYYP29wQJ3Q5k
3 | password VY5sj6z5qjxZsvdvf8yNvKcQ
4 |
--------------------------------------------------------------------------------
/test/fixtures/.netrc.template:
--------------------------------------------------------------------------------
1 | machine localhost login xyYBxzSPYYP29wQJ3Q5k password VY5sj6z5qjxZsvdvf8yNvKcQ
--------------------------------------------------------------------------------
/shell/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem 'pry', '~> 0.10.1'
4 | gem 'netrc', '~> 0.7.7'
5 | # gem 'looker-sdk', :git => 'git@github.com:looker/looker-sdk-ruby.git'
6 | gem 'looker-sdk', :path => '../'
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | rvm:
3 | - 2.0
4 | - 2.1
5 | - 2.3.1
6 | - jruby-1.7.27
7 | - jruby-9.1.5.0
8 | cache:
9 | bundler: true
10 | matrix:
11 | fast_finish: true
12 | allow_failures:
13 | - rvm: ruby-head
14 | - rvm: jruby-head
15 | - rvm: ree
16 |
--------------------------------------------------------------------------------
/shell/.gitignore:
--------------------------------------------------------------------------------
1 | # Numerous always-ignore extensions
2 | *.diff
3 | *.err
4 | *.orig
5 | *.log
6 | *.rej
7 | *.swo
8 | *.swp
9 | *.vi
10 | *~
11 | *.sass-cache
12 | *.iml
13 |
14 | # OS or Editor folders
15 | .DS_Store
16 | .cache
17 | .project
18 | .settings
19 | .tmproj
20 | nbproject
21 | Thumbs.db
22 |
23 | # Folders to ignore
24 | intermediate
25 | publish
26 | target
27 | .idea
28 | out
29 |
30 | # markdown previews
31 | *.md.html
32 |
33 | # bundler suggested ignores
34 | *.gem
35 | *.rbc
36 | .bundle
37 | .config
38 | .yardoc
39 | Gemfile.lock
40 | Gemfile.optional.lock
41 | tmp
42 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | group :development do
4 | gem 'awesome_print', '~>1.6.1', :require => 'ap'
5 | gem 'redcarpet', '~>3.1.2', :platforms => :ruby
6 | end
7 |
8 | group :development, :test do
9 | gem 'rake', '< 11.0'
10 | end
11 |
12 | group :test do
13 | # gem 'json', '~> 1.7', :platforms => [:jruby] look TODO needed?
14 | gem 'minitest', '5.9.1'
15 | gem 'mocha', '1.1.0'
16 | gem 'rack', '1.6.4'
17 | gem 'rack-test', '0.6.2'
18 | gem 'netrc', '~> 0.7.7'
19 | gem 'simplecov', '~> 0.7.1', :require => false
20 | end
21 |
22 | gemspec
23 |
--------------------------------------------------------------------------------
/shell/readme.md:
--------------------------------------------------------------------------------
1 | # [Looker](http://looker.com/) SDK Ruby Shell
2 |
3 | The Looker SDK Shell allows you to call and experiment with Looker SDK APIs
4 | in a [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop) interactive command-line environment.
5 |
6 | ### SDK Shell Setup
7 | ```bash
8 | $ cd looker-sdk/shell
9 | $ bundle install
10 | ```
11 |
12 | ### Running the Shell
13 | ```bash
14 | $ ruby shell.rb
15 | ```
16 | The Looker SDK Shell expects to find Looker API authentication credentials in
17 | a .netrc file in the current directory. See the [Looker SDK readme](../readme.md) for details
18 | on setting up your .netrc file.
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Numerous always-ignore extensions
2 | *.diff
3 | *.err
4 | *.orig
5 | *.log
6 | *.rej
7 | *.swo
8 | *.swp
9 | *.vi
10 | *~
11 | *.sass-cache
12 | *.iml
13 |
14 | # OS or Editor folders
15 | .DS_Store
16 | .cache
17 | .project
18 | .settings
19 | .tmproj
20 | nbproject
21 | Thumbs.db
22 |
23 | # Folders to ignore
24 | intermediate
25 | publish
26 | target
27 | .idea
28 | out
29 |
30 | # markdown previews
31 | *.md.html
32 |
33 | # bundler suggested ignores
34 | *.gem
35 | *.rbc
36 | .bundle
37 | .config
38 | .yardoc
39 | Gemfile.lock
40 | Gemfile.optional.lock
41 | InstalledFiles
42 | _yardoc
43 | coverage
44 | doc/
45 | lib/bundler/man
46 | pkg
47 | rdoc
48 | spec/reports
49 | test/tmp
50 | test/version_tmp
51 | tmp
52 | *.netrc
53 |
54 | # credentials for testing
55 | .netrc
56 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to Contribute
2 |
3 | We'd love to accept your patches and contributions to this project. There are
4 | just a few small guidelines you need to follow.
5 |
6 | ## Contributor License Agreement
7 |
8 | Contributions to this project must be accompanied by a Contributor License
9 | Agreement (CLA). You (or your employer) retain the copyright to your
10 | contribution; this simply gives us permission to use and redistribute your
11 | contributions as part of the project. Head over to
12 | to see your current agreements on file or
13 | to sign a new one.
14 |
15 | You generally only need to submit a CLA once, so if you've already submitted one
16 | (even if it was for a different project), you probably don't need to do it
17 | again.
18 |
19 | ## Code Reviews
20 |
21 | All submissions, including submissions by project members, require review. We
22 | use GitHub pull requests for this purpose. Consult
23 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
24 | information on using pull requests.
25 |
26 | ## Community Guidelines
27 |
28 | This project follows
29 | [Google's Open Source Community Guidelines](https://opensource.google/conduct/).
30 |
--------------------------------------------------------------------------------
/looker-sdk.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | $:.push File.expand_path('../lib', __FILE__)
3 | require 'looker-sdk/version'
4 |
5 | Gem::Specification.new do |s|
6 | s.name = 'looker-sdk'
7 | s.version = LookerSDK::VERSION
8 | s.date = "#{Time.now.strftime('%F')}"
9 | s.authors = ['Looker']
10 | s.email = 'opensource+sdkruby@looker.com'
11 | s.homepage = 'https://github.com/looker/looker-sdk-ruby'
12 | s.summary = %q{Looker Ruby SDK}
13 | s.description = 'Use this SDK to access the Looker API. The Looker API provides functions to perform administrative '+
14 | 'tasks such as provisioning users, configuring database connections, and so on. It also enables you to leverage '+
15 | 'the Looker data analytics engine to fetch data or render visualizations defined in your Looker data models. '+
16 | 'For more information, see https://looker.com.'
17 | s.license = 'MIT'
18 | s.required_ruby_version = '>= 2.5'
19 | s.requirements = 'Looker version 4.0 or later' # informational
20 |
21 | s.files = `git ls-files`.split("\n")
22 | s.test_files = `git ls-files -- {test}/*`.split("\n")
23 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
24 | s.require_paths = %w(lib)
25 | s.add_dependency 'jruby-openssl' if s.platform == :jruby
26 | s.add_dependency 'sawyer', '~> 0.8'
27 | s.add_dependency 'faraday', ['>= 1.2', '< 2.0']
28 | end
29 |
--------------------------------------------------------------------------------
/examples/me.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | require './sdk_setup'
26 |
27 | puts sdk.me.inspect
28 |
--------------------------------------------------------------------------------
/lib/looker-sdk/version.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | module LookerSDK
26 |
27 | # Current version
28 | # @return [String]
29 | VERSION = "0.1.1"
30 |
31 | end
32 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | require "bundler/gem_tasks"
26 |
27 | require "rake/testtask"
28 | Rake::TestTask.new do |t|
29 | t.libs << "test"
30 | t.test_files = FileList["test/**/test_*.rb"]
31 | t.verbose = true
32 | end
33 |
34 | task :default => :test
35 |
--------------------------------------------------------------------------------
/examples/users_with_credentials_google.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | require './sdk_setup'
26 |
27 | users = sdk.all_users(:fields => 'id, credentials_google').
28 | select {|u| u.credentials_google}
29 |
30 | users.each {|u| puts "#{u.id},#{u.credentials_google.email}"}
31 |
--------------------------------------------------------------------------------
/examples/users_with_credentials_email.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | require './sdk_setup'
26 |
27 | users = sdk.all_users(:fields => 'id, is_disabled, credentials_email').
28 | select {|u| !u.is_diabled && u.credentials_email && u.credentials_email.email}
29 |
30 | users.each {|u| puts "#{u.id},#{u.credentials_email.email}"}
31 |
--------------------------------------------------------------------------------
/examples/users_with_credentials_google_without_credentials_email.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | require './sdk_setup'
26 |
27 | users = sdk.all_users(:fields => 'id, is_disabled, credentials_email, credentials_google').
28 | select {|u| !u.is_diabled && u.credentials_google && u.credentials_google.email && !u.credentials_email}
29 |
30 | users.each {|u| puts "#{u.id},#{u.credentials_google.email}"}
31 |
--------------------------------------------------------------------------------
/examples/refresh_user_notification_addresses.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | require './sdk_setup'
26 |
27 | sdk.all_users(:fields => 'id,email').each do |user|
28 | new_user = sdk.update_user(user.id, {})
29 | if user.email == new_user.email
30 | puts "No Change for #{user.id}"
31 | else
32 | puts "Refreshed #{user.id}. Old email '#{user.email}'. Refreshed email: '#{new_user.email}'."
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/examples/users_with_credentials_embed.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | require './sdk_setup'
26 |
27 | users = sdk.all_users(fields:'id, is_disabled, display_name, credentials_embed').map do |u|
28 | next if u.is_diabled || u.credentials_embed.empty?
29 | creds = u.credentials_embed.first
30 | [u.id, u.display_name, creds.external_user_id, creds.external_group_id, creds.logged_in_at]
31 | end.compact
32 |
33 | users.each{|u| p u}
34 |
--------------------------------------------------------------------------------
/lib/looker-sdk/response/raise_error.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | require 'faraday'
26 | require 'looker-sdk/error'
27 |
28 | module LookerSDK
29 | # Faraday response middleware
30 | module Response
31 |
32 | # HTTP status codes returned by the API
33 | class RaiseError < Faraday::Middleware
34 |
35 | def on_complete(response)
36 | if error = LookerSDK::Error.from_response(response)
37 | raise error
38 | end
39 | end
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/examples/delete_all_user_sessions.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | require './sdk_setup'
26 |
27 | total_count = 0
28 | sdk.all_users(:fields => 'id, display_name').each do |user|
29 | count = 0
30 | sdk.all_user_sessions(user.id, :fields => 'id').each do |session|
31 | sdk.delete_user_session(user.id, session.id)
32 | count += 1
33 | end
34 | puts "Deleted #{count} sessions for #{user.id} #{user.display_name}"
35 | total_count += count
36 | end
37 | puts "Deleted #{total_count} sessions"
38 |
39 |
40 |
--------------------------------------------------------------------------------
/examples/delete_credentials_google_for_users.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | require './sdk_setup'
26 |
27 | $stdin.each_line do |line|
28 | line.chomp!
29 |
30 | id, _ = line.split(',', 2).map(&:strip)
31 |
32 | begin
33 | user = sdk.user(id)
34 | if user.credentials_google
35 | sdk.delete_user_credentials_google(id)
36 | puts "Success: Deleted credentials_google for User with id '#{id}'"
37 | else
38 | puts "Error: User with id '#{id}' Does not have credentials_google"
39 | end
40 | rescue LookerSDK::NotFound
41 | puts "Error: User with id '#{id}' Not found"
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/examples/generate_password_reset_tokens_for_users.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | require './sdk_setup'
26 |
27 | $stdin.each_line do |line|
28 | line.chomp!
29 |
30 | id = line.split(',', 2).map(&:strip).first
31 |
32 | begin
33 | user = sdk.user(id)
34 | if user.credentials_email
35 | token = sdk.create_user_credentials_email_password_reset(id)
36 | puts "#{token.email},#{token.password_reset_url}"
37 | else
38 | puts "Error: User with id '#{id}' Does not have credentials_email"
39 | end
40 | rescue LookerSDK::NotFound
41 | puts "Error: User with id '#{id}' Not found"
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/examples/create_credentials_email_for_users.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | require './sdk_setup'
26 |
27 | $stdin.each_line do |line|
28 | line.chomp!
29 |
30 | id, email = line.split(',', 2).map(&:strip)
31 |
32 | begin
33 | user = sdk.user(id)
34 | if user.credentials_email
35 | puts "Error: User with id '#{id}' Already has credentials_email"
36 | else
37 | sdk.create_user_credentials_email(id, {:email => email})
38 | puts "Success: Created credentials_email for User with id '#{id}' and email '#{email}'"
39 | end
40 | rescue LookerSDK::NotFound
41 | puts "Error: User with id '#{id}' Not found"
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/examples/sdk_setup.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | require 'rubygems'
26 | require 'bundler/setup'
27 |
28 | require 'looker-sdk'
29 |
30 | # common file used by various examples to setup and init sdk
31 |
32 | def sdk
33 | @sdk ||= LookerSDK::Client.new(
34 | :netrc => true,
35 | :netrc_file => "./.netrc",
36 |
37 | # use my local looker with self-signed cert
38 | :connection_options => {:ssl => {:verify => false}},
39 | :api_endpoint => "https://localhost:19999/api/3.0",
40 |
41 | # use a real looker the way you are supposed to!
42 | # :connection_options => {:ssl => {:verify => true}},
43 | # :api_endpoint => "https://mycoolcompany.looker.com:19999/api/3.0",
44 | )
45 | end
46 |
--------------------------------------------------------------------------------
/examples/change_credentials_email_address_for_users.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | require './sdk_setup'
26 |
27 | users = Hash[
28 | sdk.all_users(:fields => 'id, credentials_email').
29 | map{|u| [u.credentials_email.email, u.id] if u.credentials_email }.compact
30 | ]
31 |
32 | $stdin.each_line do |line|
33 | line.chomp!
34 |
35 | _, old_email, new_email = line.split(',', 3).map(&:strip)
36 |
37 | if id = users[old_email]
38 | begin
39 | sdk.update_user_credentials_email(id, {:email => new_email})
40 | puts "Successfully changed '#{old_email}' => '#{new_email}'"
41 | rescue => e
42 | puts "FAILED to changed '#{old_email}' => '#{new_email}' because of: #{e.class}:#{e.message}"
43 | end
44 | else
45 | puts "FAILED: Could not find user with email '#{old_email}'"
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/examples/roles_and_users_with_permission.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | require './sdk_setup'
26 |
27 |
28 | while true do
29 | print "permission [,model]? "
30 | permission_name, model_name = gets.chomp.split(',')
31 |
32 | break if permission_name.empty?
33 |
34 | roles = sdk.all_roles.select do |role|
35 | (role.permission_set.all_access || role.permission_set.permissions.join(',').include?(permission_name)) &&
36 | (model_name.nil? || role.model_set.all_access || role.model_set.models.join(',').include?(model_name))
37 | end
38 |
39 | puts "Roles: #{roles.map(&:name).join(', ')}"
40 |
41 | role_ids = roles.map(&:id)
42 | users = sdk.all_users.select {|user| (user.role_ids & role_ids).any?}
43 | user_names = users.map{|u| "#{u.id}#{" ("+u.display_name+")" if u.display_name}"}.join(', ')
44 |
45 | puts "Users: #{user_names}"
46 | end
47 |
--------------------------------------------------------------------------------
/examples/streaming_downloads.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | require './sdk_setup'
26 |
27 | # This snippet shows how to download sdk responses using http streaming.
28 | # Streaming processes the download in chunks which you can write
29 | # to file or perform other processing on without having to wait for the entire
30 | # response to download first. Streaming can be very memory efficient
31 | # for handling large result sets compared to just downloading the whole thing into
32 | # a Ruby object in memory.
33 |
34 | def run_look_to_file(look_id, filename, format, opts = {})
35 | File.open(filename, 'w') do |file|
36 | sdk.run_look(look_id, format, opts) do |data, progress|
37 | file.write(data)
38 | puts "Wrote #{data.length} bytes of #{progress.length} total"
39 | end
40 | end
41 | end
42 |
43 | # Replace the look id (38) with the id of your actual look
44 | run_look_to_file(38, 'out.csv', 'csv', limit: 10000)
45 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Looker Data Sciences, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
23 | The Looker SDK for Ruby is based in part on [Octokit](https://github.com/octokit/octokit.rb).
24 |
25 | Octokit's license:
26 |
27 | Copyright (c) 2009-2014 Wynn Netherland, Adam Stacoviak, Erik Michaels-Ober
28 |
29 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
30 |
31 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
32 |
33 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
34 |
--------------------------------------------------------------------------------
/lib/looker-sdk/sawyer_patch.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | # Make Sawyer decode the body lazily.
26 | # This is a temp monkey-patch until sawyer has: https://github.com/lostisland/sawyer/pull/31
27 | # At that point we can remove this and update our dependency to the new Sawyer release version.
28 |
29 | module Sawyer
30 | class Response
31 |
32 | attr_reader :env, :body
33 |
34 | def initialize(agent, res, options = {})
35 | @agent = agent
36 | @status = res.status
37 | @headers = res.headers
38 | @env = res.env
39 | @body = res.body
40 | @rels = process_rels
41 | @started = options[:sawyer_started]
42 | @ended = options[:sawyer_ended]
43 | end
44 |
45 | def data
46 | @data ||= begin
47 | return(body) unless (headers[:content_type] =~ /json|msgpack/)
48 | process_data(agent.decode_body(body))
49 | end
50 | end
51 |
52 | def inspect
53 | %(#<#{self.class}: #{@status} @rels=#{@rels.inspect} @data=#{data.inspect}>)
54 | end
55 |
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/test/helper.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | require 'simplecov'
26 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
27 | SimpleCov::Formatter::HTMLFormatter
28 | ]
29 | SimpleCov.start
30 |
31 | require 'rubygems'
32 | require 'bundler/setup'
33 |
34 | require 'ostruct'
35 | require 'json'
36 | require 'looker-sdk'
37 |
38 | require 'minitest/autorun'
39 | require 'minitest/spec'
40 | require 'minitest/mock'
41 | require 'mocha/mini_test'
42 | require "rack/test"
43 | require "rack/request"
44 |
45 | def fixture_path
46 | File.expand_path("../fixtures", __FILE__)
47 | end
48 |
49 | def fix_netrc_permissions(path)
50 | s = File.stat(path)
51 | raise "'#{path}'' not a file" unless s.file?
52 | File.chmod(0600, path) unless s.mode.to_s(8)[3..5] == "0600"
53 | end
54 |
55 | fix_netrc_permissions(File.join(fixture_path, '.netrc'))
56 |
57 | def setup_sdk
58 | LookerSDK.reset!
59 | LookerSDK.configure do |c|
60 | c.lazy_swagger = true
61 | c.connection_options = {:ssl => {:verify => false}}
62 | c.netrc = true
63 | c.netrc_file = File.join(fixture_path, '.netrc')
64 | end
65 | end
66 |
67 | def teardown_sdk
68 | setup_sdk # put back initial config
69 | LookerSDK.logout
70 | end
71 |
--------------------------------------------------------------------------------
/lib/looker-sdk/rate_limit.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | module LookerSDK
26 |
27 | # Class for API Rate Limit info
28 | #
29 | # @!attribute [w] limit
30 | # @return [Fixnum] Max tries per rate limit period
31 | # @!attribute [w] remaining
32 | # @return [Fixnum] Remaining tries per rate limit period
33 | # @!attribute [w] resets_at
34 | # @return [Time] Indicates when rate limit resets
35 | # @!attribute [w] resets_in
36 | # @return [Fixnum] Number of seconds when rate limit resets
37 | #
38 | # @see look TODO docs link
39 | class RateLimit < Struct.new(:limit, :remaining, :resets_at, :resets_in)
40 |
41 | # Get rate limit info from HTTP response
42 | #
43 | # @param response [#headers] HTTP response
44 | # @return [RateLimit]
45 | def self.from_response(response)
46 | info = new
47 | if response && !response.headers.nil?
48 | info.limit = (response.headers['X-RateLimit-Limit'] || 1).to_i
49 | info.remaining = (response.headers['X-RateLimit-Remaining'] || 1).to_i
50 | info.resets_at = Time.at((response.headers['X-RateLimit-Reset'] || Time.now).to_i)
51 | info.resets_in = (info.resets_at - Time.now).to_i
52 | end
53 |
54 | info
55 | end
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/shell/shell.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | require 'rubygems'
26 | require 'bundler/setup'
27 |
28 | require 'looker-sdk.rb'
29 | require 'pry'
30 |
31 | def sdk
32 | @sdk ||= LookerSDK::Client.new(
33 | # Create your own API3 key and add it to a .netrc file in your location of choice.
34 | :netrc => true,
35 | :netrc_file => "./.netrc",
36 |
37 | # Disable cert verification if the looker has a self-signed cert.
38 | # :connection_options => {:ssl => {:verify => false}},
39 |
40 | # Support self-signed cert *and* set longer timeout to allow for long running queries.
41 | :connection_options => {:ssl => {:verify => false}, :request => {:timeout => 60 * 60, :open_timeout => 30}},
42 |
43 | :api_endpoint => "https://localhost:19999/api/3.0",
44 |
45 | # Customize to use your specific looker instance
46 | # :connection_options => {:ssl => {:verify => true}},
47 | # :api_endpoint => "https://looker.mycoolcompany.com:19999/api/3.0",
48 | )
49 | end
50 |
51 | begin
52 | puts "Connecting to Looker at '#{sdk.api_endpoint}'"
53 | puts sdk.alive? ? "Looker is alive!" : "Sad Looker, can't connect:\n #{sdk.last_error}"
54 | puts sdk.authenticated? ? "Authenticated!" : "Sad Looker, can't authenticate:\n #{sdk.last_error}"
55 |
56 | binding.pry self
57 | rescue Exception => e
58 | puts e
59 | ensure
60 | puts 'Bye!'
61 | end
62 |
63 |
--------------------------------------------------------------------------------
/examples/ldap_roles_test.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | require './sdk_setup'
26 |
27 | ############################################################################################
28 | # simulate a list read from file
29 |
30 | user_list = <<-ENDMARK
31 |
32 | mward
33 | mwhite
34 | missing
35 |
36 | ENDMARK
37 |
38 | ############################################################################################
39 | # helpers
40 |
41 | def ldap_config
42 | @ldap_config ||= sdk.ldap_config.to_attrs
43 | end
44 |
45 | def groups_map_for_config
46 | @groups_map_for_config ||= ldap_config[:groups].map do |group|
47 | {:name => group[:name], :role_ids => group[:roles].map{|role| role[:id]}}
48 | end
49 | end
50 |
51 | def test_ldap_user(user)
52 | params = ldap_config.merge({:groups_with_role_ids => groups_map_for_config, :test_ldap_user => user})
53 | sdk.test_ldap_config_user_info(params)
54 | end
55 |
56 | ############################################################################################
57 | # process the list and puts results
58 |
59 | user_list.each_line do |user|
60 | user.strip!
61 | next if user.empty?
62 |
63 | puts "'#{user}' ..."
64 |
65 | result = test_ldap_user(user).to_attrs
66 | if result[:status] == 'success'
67 | puts "Success"
68 | puts result[:user]
69 | else
70 | puts "FAILURE"
71 | puts result
72 | end
73 | puts
74 | end
75 |
--------------------------------------------------------------------------------
/examples/convert_look_to_lookless_tile.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | require 'looker-sdk'
26 | require 'json'
27 |
28 | # Note that this example requires API 3.1 for new dashboard element manipulation functions
29 |
30 | sdk = LookerSDK::Client.new(
31 | :client_id => ENV['KEY'],
32 | :client_secret => ENV['SECRET'],
33 |
34 | # API 3.1 URL would look like: https://myhost.com:19999/api/3.1
35 | :api_endpoint => ENV['URL']
36 | )
37 | #Set the dashboard here.
38 | dashboard_id = ENV['DASHBOARD_ID']
39 | # Get the dashboard we want to convert and get elements
40 |
41 | dashboard = sdk.dashboard(dashboard_id).to_h
42 |
43 | elements = dashboard[:dashboard_elements]
44 | if dashboard then puts "Dashboard has been received" end
45 |
46 | for element in elements
47 |
48 | # Extract important IDs
49 |
50 | element_id = element[:id]
51 | look_id = element[:look_id]
52 | query_id = element[:query_id]
53 |
54 | # If look_id is non-null and query_id is null, tile is a Look-based tile
55 | if look_id && query_id.nil?
56 |
57 | # Get the Look so we can get its query_id
58 | look = sdk.look(look_id).to_h
59 | query_id = look[:query_id]
60 |
61 | # Update the tile to have a null look_id and update query_id
62 | sdk.update_dashboard_element(element_id,
63 | {
64 | "look_id": nil,
65 | "query_id": query_id,
66 | }
67 | )
68 | puts "Tile #{element_id.to_s} has been updated in Dashboard #{dashboard_id.to_s}"
69 |
70 | end
71 | end
72 |
--------------------------------------------------------------------------------
/lib/looker-sdk.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | require 'faraday'
26 | # The Faraday autoload scheme is supposed to work to load other dependencies on demand.
27 | # It does work, but there are race condition problems upon first load of a given file
28 | # that have caused intermittent failures in our ruby and integration tests - and could bite in production.
29 | # The simple/safe solution is to just pre-require these parts that are actually used
30 | # by the looker-sdk to prevent a race condition later.
31 | # See https://github.com/lostisland/faraday/issues/181
32 | # and https://bugs.ruby-lang.org/issues/921
33 | require 'faraday/autoload'
34 | require 'faraday/adapter'
35 | require 'faraday/adapter/rack'
36 | require 'faraday/adapter/net_http'
37 | require 'faraday/connection'
38 | require 'faraday/error'
39 | require 'faraday/middleware'
40 | require 'faraday/options'
41 | require 'faraday/parameters'
42 | require 'faraday/rack_builder'
43 | require 'faraday/request'
44 | require 'faraday/request/authorization'
45 | require 'faraday/response'
46 | require 'faraday/utils'
47 |
48 | #require 'rack'
49 | #require 'rack/mock_response'
50 |
51 | require 'looker-sdk/client'
52 | require 'looker-sdk/default'
53 |
54 | module LookerSDK
55 |
56 | class << self
57 | include LookerSDK::Configurable
58 |
59 | # API client based on configured options {Configurable}
60 | #
61 | # @return [LookerSDK::Client] API wrapper
62 | def client
63 | @client = LookerSDK::Client.new(options) unless defined?(@client) && @client.same_options?(options)
64 | @client
65 | end
66 |
67 | # @private
68 | def respond_to_missing?(method_name, include_private=false); client.respond_to?(method_name, include_private); end if RUBY_VERSION >= "1.9"
69 | # @private
70 | def respond_to?(method_name, include_private=false); client.respond_to?(method_name, include_private) || super; end if RUBY_VERSION < "1.9"
71 |
72 | private
73 |
74 | def method_missing(method_name, *args, &block)
75 | return super unless client.respond_to?(method_name)
76 | client.send(method_name, *args, &block)
77 | end
78 |
79 | end
80 | end
81 |
82 | LookerSDK.setup
83 |
--------------------------------------------------------------------------------
/streaming.md:
--------------------------------------------------------------------------------
1 | ### Streaming Downloads
2 |
3 | #### Beta Feature - Experimental!
4 |
5 | This SDK makes it easy to fetch a response from a Looker API and hydrate it into a Ruby object.This convenience is great for working with configuration and administrative data. However, when the response is gigabytes of row data, pulling it all into memory doesn't work so well - you can't begin processing the data until after it has all downloaded, for example, and chewing up tons of memory will put a serious strain on the entire system - even crash it.
6 |
7 | One solution to all this is to use streaming downloads to process the data in chunks as it is downloaded. Streaming requires a little more code to set up but the benefits can be significant.
8 |
9 | To use streaming downloads with the Looker SDK, simply add a block to an SDK call. The block will be called with chunks of data as they are downloaded instead of the method returning a complete result.
10 |
11 | For example:
12 |
13 | ```ruby
14 | def run_look_to_file(look_id, filename, format, opts = {})
15 | File.open(filename, 'w') do |file|
16 | sdk.run_look(look_id, format, opts) do |data, progress|
17 | file.write(data)
18 | puts "Wrote #{data.length} bytes of #{progress.length} total"
19 | end
20 | end
21 | end
22 |
23 | run_look_to_file(38, 'out.csv', 'csv', limit: 10000)
24 | ```
25 |
26 | In the code above, `sdk.run_look` opens a statement block. The code in the block will be called with chunks of data. The output looks like this:
27 | ```
28 | Wrote 16384 bytes of 16384 total
29 | Wrote 16384 bytes of 32768 total
30 | Wrote 7327 bytes of 40095 total
31 | Wrote 16384 bytes of 56479 total
32 | etc...
33 | ```
34 |
35 | You can also abort a streaming download by calling `progress.stop` within the block, like this:
36 | ```ruby
37 | sdk.run_look(look_id, format, opts) do |data, progress|
38 | if some_condition
39 | progress.stop
40 | else
41 | process_data(data)
42 | end
43 | end
44 |
45 | ```
46 |
47 | ##### Streaming Caveats
48 | * You won't know in advance how many bytes are in the response. Blocks arrive until there aren't any more.
49 | * If the connection to the Looker server is broken while streaming, it will have the same appearance as normal end-of-file stream termination.
50 | * The HTTP status in the response arrives before the response body begins downloading. If an error occurs during the download, it cannot be communicated via HTTP status. It is quite possible to have an HTTP status 200 OK and later discover an error in the data stream. If the connection between the Looker server and the SQL database is severed while you are streaming results, for example, Looker will append an error message to the data you receive and terminate the streaming session.
51 |
52 | These caveats can be mitigated by knowing the structure of the data being streamed. Row data in JSON format will be an array of objects, for example. If the data received is missing the closing `]` then you know the stream download ended prematurely.
53 |
54 | #### A Tale of Two Stacks
55 |
56 | The Looker Ruby SDK is built on top of Sawyer, and Sawyer sits on top of Faraday. Faraday does not support HTTP Streaming, so to do our streaming stuff we have to bypass Faraday and talk directly to the Net:HTTP stack. Our streaming implementation gathers connection settings from the Faraday stack so there's no additional config required for http streaming.
57 |
58 | Streaming downloads have not been tested with proxy connections. We assume attempting to stream across an http proxy will not work.
59 |
60 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | #############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2014 Zee Spencer
5 | # Copyright (c) 2020 Google LLC
6 | #
7 | # Permission is hereby granted, free of charge, to any person obtaining a copy
8 | # of this software and associated documentation files (the "Software"), to deal
9 | # in the Software without restriction, including without limitation the rights
10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | # copies of the Software, and to permit persons to whom the Software is
12 | # furnished to do so, subject to the following conditions:
13 | #
14 | # The above copyright notice and this permission notice shall be included in all
15 | # copies or substantial portions of the Software.
16 | #
17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | # SOFTWARE.
24 | #############################################################################################
25 |
26 | # Allows running (and re-running) of tests against several ruby versions,
27 | # assuming you use rbenv instead of rvm.
28 |
29 | # Uses pattern rules (task-$:) and automatic variables ($*).
30 | # Pattern rules: http://ftp.gnu.org/old-gnu/Manuals/make-3.79.1/html_chapter/make_10.html#SEC98
31 | # Automatic variables: http://ftp.gnu.org/old-gnu/Manuals/make-3.79.1/html_chapter/make_10.html#SEC101
32 |
33 | # Rbenv-friendly version identifiers for supported Rubys
34 | 25_version = 2.5.7
35 | jruby_92160_version = jruby-9.2.16.0
36 |
37 | # The ruby version for use in a given rule.
38 | # Requires a matched pattern rule and a supported ruby version.
39 | #
40 | # Given a pattern rule defined as "install-ruby-%"
41 | # When the rule is ran as "install-ruby-193"
42 | # Then the inner addsuffix call evaluates to "193_version"
43 | # And given_ruby_version becomes "1.9.3-p551"
44 | given_ruby_version = $($(addsuffix _version, $*))
45 |
46 | # Instruct rbenv on which Ruby version to use when running a command.
47 | # Requires a pattern rule and a supported ruby version.
48 | #
49 | # Given a pattern rule defined as "test-%"
50 | # When the rule is ran as "test-187"
51 | # Then with_given_ruby becomes "RBENV_VERSION=1.8.7-p375"
52 | with_given_ruby = RBENV_VERSION=$(given_ruby_version)
53 |
54 | # Runs tests for all supported ruby versions.
55 | test: test-25 test-jruby_92160
56 |
57 | # Runs tests against a specific ruby version
58 | test-%:
59 | rm -f Gemfile.lock
60 | $(with_given_ruby) bundle install --quiet
61 | $(with_given_ruby) bundle exec rake
62 |
63 | # Installs all ruby versions and their gems
64 | install: install-25 install-jruby_92160
65 |
66 | # Install a particular ruby version
67 | install-ruby-%:
68 | rm -f Gemfile.lock
69 | rbenv install -s $(given_ruby_version)
70 |
71 | # Install gems into a specific ruby version
72 | install-gems-%:
73 | rm -f Gemfile.lock
74 | $(with_given_ruby) gem update --system
75 | $(with_given_ruby) gem install bundler
76 | $(with_given_ruby) bundle install
77 |
78 | # Installs a specific ruby version and it's gems
79 | # At the bottom so it doesn't match install-gems and install-ruby tasks.
80 | install-%:
81 | make install-ruby-$* install-gems-$*
82 |
--------------------------------------------------------------------------------
/examples/add_delete_users.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | require './sdk_setup'
26 |
27 | ############################################################################################
28 | # simulate a list read from file
29 |
30 | user_list = <<-ENDMARK
31 |
32 | Joe Williams, joe@mycoolcompany.com, Admin
33 | Jane Schumacher, jane@mycoolcompany.com, User SuperDeveloper
34 | Jim Watson, jim@mycoolcompany.com, User
35 | Jim Wu, jimw@mycoolcompany.com, User
36 |
37 | ENDMARK
38 |
39 | ############################################################################################
40 |
41 | def create_users(lines)
42 | # create a hash to use below. role.name => role.id
43 | roles_by_name = Hash[ sdk.all_roles.map{|role| [role.name, role.id] } ]
44 |
45 | # for each line, try to create that user with name, email credentials, and roles
46 | lines.each_line do |line|
47 | line.strip!
48 | next if line.empty?
49 |
50 | # quicky parsing: note lack of error handling!
51 |
52 | name, email, roles = line.split(',')
53 | fname, lname = name.split(' ')
54 | [fname, lname, email, roles].each(&:strip!)
55 |
56 | role_ids = []
57 | roles.split(' ').each do |role_name|
58 | if id = roles_by_name[role_name]
59 | role_ids << id
60 | else
61 | raise "#{role_name} does not exist. ABORTING!"
62 | end
63 | end
64 |
65 | # for display
66 | user_info = "#{fname} #{lname} <#{email}> as #{roles}"
67 |
68 | begin
69 | # call the SDK to create user with names, add email/password login credentials, set user roles
70 | user = sdk.create_user({:first_name => fname, :last_name => lname})
71 | sdk.create_user_credentials_email(user.id, {:email => email})
72 | sdk.set_user_roles(user.id, role_ids)
73 | puts "Created user: #{user_info}"
74 | rescue LookerSDK::Error => e
75 | # if any errors occur above then 'undo' by deleting the given user (if we got that far)
76 | sdk.delete_user(user.id) if user
77 | puts "FAILED to create user: #{user_info} (#{e.message}) "
78 | end
79 | end
80 | end
81 |
82 | ##################################################################
83 |
84 | def delete_users(lines)
85 | # create a hash to use below. user.credentials_email.email => user.id
86 | users_by_email = Hash[ sdk.all_users.map{|user| [user.credentials_email.email, user.id] if user.credentials_email}.compact ]
87 |
88 | lines.each_line do |line|
89 | line.strip!
90 | next if line.empty?
91 |
92 | # quicky parsing: note lack of error handling!
93 | name, email, roles = line.split(',')
94 | fname, lname = name.split(' ')
95 | [fname, lname, email, roles].each(&:strip!)
96 |
97 | # for display
98 | user_info = "#{fname} #{lname} <#{email}>"
99 |
100 | begin
101 | if id = users_by_email[email]
102 | sdk.delete_user(id)
103 | puts "Deleted user: #{user_info}>"
104 | else
105 | puts "Did not find user: #{user_info}>"
106 | end
107 | rescue LookerSDK::Error => e
108 | puts "FAILED to delete user: #{user_info} (#{e.message}) "
109 | end
110 | end
111 | end
112 |
113 | ##################################################################
114 |
115 | # call the methods
116 | create_users(user_list)
117 | puts
118 | delete_users(user_list)
119 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of
9 | experience, education, socio-economic status, nationality, personal appearance,
10 | race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or reject
41 | comments, commits, code, wiki edits, issues, and other contributions that are
42 | not aligned to this Code of Conduct, or to ban temporarily or permanently any
43 | contributor for other behaviors that they deem inappropriate, threatening,
44 | offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | This Code of Conduct also applies outside the project spaces when the Project
56 | Steward has a reasonable belief that an individual's behavior may have a
57 | negative impact on the project or its community.
58 |
59 | ## Conflict Resolution
60 |
61 | We do not believe that all conflict is bad; healthy debate and disagreement
62 | often yield positive results. However, it is never okay to be disrespectful or
63 | to engage in behavior that violates the project’s code of conduct.
64 |
65 | If you see someone violating the code of conduct, you are encouraged to address
66 | the behavior directly with those involved. Many issues can be resolved quickly
67 | and easily, and this gives people more control over the outcome of their
68 | dispute. If you are unable to resolve the matter for any reason, or if the
69 | behavior is threatening or harassing, report it. We are dedicated to providing
70 | an environment where participants feel welcome and safe.
71 |
72 | Reports should be directed to *Mike DeAngelo* drstrangelove@google.com, the
73 | Project Steward(s) for *looker-sdk-ruby*. It is the Project Steward’s duty to
74 | receive and address reported violations of the code of conduct. They will then
75 | work with a committee consisting of representatives from the Open Source
76 | Programs Office and the Google Open Source Strategy team. If for any reason you
77 | are uncomfortable reaching out to the Project Steward, please email
78 | opensource@google.com.
79 |
80 | We will investigate every complaint, but you may not receive a direct response.
81 | We will use our discretion in determining when and how to follow up on reported
82 | incidents, which may range from not taking action to permanent expulsion from
83 | the project and project-sponsored spaces. We will notify the accused of the
84 | report and provide them an opportunity to discuss it before any action is taken.
85 | The identity of the reporter will be omitted from the details of the report
86 | supplied to the accused. In potentially harmful situations, such as ongoing
87 | harassment or threats to anyone's safety, we may take action without notice.
88 |
89 | ## Attribution
90 |
91 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4,
92 | available at
93 | https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
94 |
--------------------------------------------------------------------------------
/lib/looker-sdk/authentication.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | module LookerSDK
26 |
27 | # Authentication methods for {LookerSDK::Client}
28 | module Authentication
29 |
30 | attr_accessor :access_token_type, :access_token_expires_at
31 |
32 | # This is called automatically by 'request'
33 | def ensure_logged_in
34 | authenticate unless token_authenticated? || @skip_authenticate
35 | end
36 |
37 | def without_authentication
38 | begin
39 | old_skip = @skip_authenticate || false
40 | @skip_authenticate = true
41 | yield
42 | ensure
43 | @skip_authenticate = old_skip
44 | end
45 | end
46 |
47 | # Authenticate to the server and get an access_token for use in future calls.
48 |
49 | def authenticate
50 | raise "client_id and client_secret required" unless application_credentials?
51 |
52 | set_access_token_from_params(nil)
53 | without_authentication do
54 | encoded_auth = Faraday::Utils.build_query(application_credentials)
55 | post("#{URI.parse(api_endpoint).path}/login", encoded_auth, header: {:'Content-Type' => 'application/x-www-form-urlencoded'})
56 | raise "login failure #{last_response.status}" unless last_response.status == 200
57 | set_access_token_from_params(last_response.data)
58 | end
59 | end
60 |
61 | def set_access_token_from_params(params)
62 | reset_agent
63 | if params
64 | @access_token = params[:access_token]
65 | @access_token_type = params[:token_type]
66 | @access_token_expires_at = Time.now + params[:expires_in]
67 | else
68 | @access_token = @access_token_type = @access_token_expires_at = nil
69 | end
70 | end
71 |
72 | def logout
73 | without_authentication do
74 | result = !!@access_token && ((delete("#{URI.parse(api_endpoint).path}/logout") ; delete_succeeded?) rescue false)
75 | set_access_token_from_params(nil)
76 | result
77 | end
78 | end
79 |
80 |
81 | # Indicates if the client has OAuth Application
82 | # client_id and client_secret credentials
83 | #
84 | # @see look TODO docs link
85 | # @return Boolean
86 | def application_credentials?
87 | !!application_credentials
88 | end
89 |
90 | # Indicates if the client has an OAuth
91 | # access token
92 | #
93 | # @see look TODO docs link
94 | # @return [Boolean]
95 | def token_authenticated?
96 | !!(@access_token && (@access_token_expires_at.nil? || @access_token_expires_at > Time.now))
97 | end
98 |
99 | private
100 |
101 | def application_credentials
102 | if @client_id && @client_secret
103 | {
104 | :client_id => @client_id,
105 | :client_secret => @client_secret
106 | }
107 | end
108 | end
109 |
110 | def load_credentials_from_netrc
111 | return unless netrc?
112 |
113 | require 'netrc'
114 | info = Netrc.read File.expand_path(netrc_file)
115 | netrc_host = URI.parse(api_endpoint).host
116 | creds = info[netrc_host]
117 | if creds.nil?
118 | # creds will be nil if there is no netrc for this end point
119 | looker_warn "Error loading credentials from netrc file for #{api_endpoint}"
120 | else
121 | self.client_id = creds[0]
122 | self.client_secret = creds[1]
123 | end
124 | rescue LoadError
125 | looker_warn "Please install netrc gem for .netrc support"
126 | end
127 |
128 | end
129 | end
130 |
--------------------------------------------------------------------------------
/authentication.md:
--------------------------------------------------------------------------------
1 | ## How to authenticate to Looker's API 3
2 |
3 | The preferred way to authenticate is to use the looker SDK to manage the login and the passing of
4 | access_tokens as needed. This doc, however, explains how to do this authentication in a generic way without the SDK and using curl instead for illustration.
5 |
6 | Looker API 3 implements OAuth 2's "Resource Owner Password Credentials Grant" pattern,
7 | See: http://tools.ietf.org/html/rfc6749#section-4.3
8 |
9 | ### Setup an API key
10 | An 'API 3' key is required in order to login and use the API. The key consists of a client_id and a client_secret.
11 | 'Login' consists of using these credentials to generate a short-term access_token which is then used to make API calls.
12 |
13 | The client_id could be considered semi-public. While, the client_secret is a secret password and MUST be
14 | carefully protected. These credentials should not be hard-coded into client code. They should be read from
15 | a closely guarded data file when used by client processes.
16 |
17 | Admins can create an API 3 key for a user on looker's user edit page. All requests made using these
18 | credentials are made 'as' that user and limited to the role permissions specified for that user. A user
19 | account with an appropriate role may be created as needed for the API client's use.
20 |
21 | Note that API 3 tokens should be created for 'regular' Looker users and *not* via the legacy 'Add API User' button.
22 |
23 |
24 | ### Ensure that the API is accessible
25 | Looker versions 3.4 (and beyond) expose the API via a port different from the port used by the web app.
26 | The default port is 19999. It may be necessary to have the Ops team managing the looker instance ensure that this
27 | port is made accessible network-wise to client software running on non-local hosts.
28 |
29 | The '/alive' url can be used to detect if the server is reachable
30 |
31 |
32 | ### Login
33 | To access the API it is necessary to 'login' using the client_id and client_secret in order to acquire an
34 | access_token that will be used in actual API requests. This is done by POSTing to the /login url. The access_token is returned in a short json body and has a limited time before it expires (the default at this point is 1 hour).
35 | An 'expires_in' field is provided to tell client software how long they should expect the token to last.
36 |
37 | A new token is created for each /login call and remains valid until it expires or is revoked via /logout.
38 |
39 | It is VERY important that these tokens never be sent in the clear or exposed in any other way.
40 |
41 |
42 | ### Call the API
43 | API calls then pass the access_token to looker using an 'Authorization' header. API calls are
44 | done using GET, PUT, POST, PATCH, or DELETE as appropriate for the specific call. Normal REST stuff.
45 |
46 |
47 | ### Logout
48 | A '/logout' url is available if the client wants to revoke an access_token. It requires a DELETE request.
49 | Looker reserves the right to limit the number of 'live' access_tokens per user.
50 |
51 | -------------------------------------------------------------------------------------------------
52 |
53 | The following is an example session using Curl. The '-i' param is used to show returned headers.
54 |
55 | The simple flow in this example is to login, get info about the current user, then logout.
56 |
57 | Note that in this example the client_id and client_secret params are passed using '-d' which causes the
58 | request to be done as a POST. And, the -H param is used to specify http headers to add for API requests.
59 |
60 | ```
61 | # Check that the port is reachable
62 | > curl -i https://localhost:19999/alive
63 | HTTP/1.1 200 OK
64 | Content-Type: application/json;charset=utf-8
65 | Vary: Accept-Encoding
66 | X-Content-Type-Options: nosniff
67 | Content-Length: 0
68 |
69 |
70 | # Do the login to get an access_token
71 | > curl -i -d "client_id=4j3SD8W5RchHw5gvZ5Yd&client_secret=sVySctSMpQQG3TzdNQ5d2dND" https://localhost:19999/login
72 | HTTP/1.1 200 OK
73 | Content-Type: application/json;charset=utf-8
74 | Vary: Accept-Encoding
75 | X-Content-Type-Options: nosniff
76 | Content-Length: 99
77 |
78 | {"access_token":"4QDkCyCtZzYgj4C2p2cj3csJH7zqS5RzKs2kTnG4","token_type":"Bearer","expires_in":3600}
79 |
80 | # Use an access_token (the token can be used over and over for API calls until it expires)
81 | > curl -i -H "Authorization: token 4QDkCyCtZzYgj4C2p2cj3csJH7zqS5RzKs2kTnG4" https://localhost:19999/api/4.0/user
82 | HTTP/1.1 200 OK
83 | Content-Type: application/json;charset=utf-8
84 | Vary: Accept-Encoding
85 | X-Content-Type-Options: nosniff
86 | Content-Length: 502
87 |
88 | {"id":14,"first_name":"Plain","last_name":"User","email":"dude+1@looker.com","models_dir":null,"is_disabled":false,"look_access":[14],"avatar_url":"https://www.gravatar.com/avatar/b7f792a6180a36a4058f36875584bc45?s=156&d=mm","credentials_email":{"email":"dude+1@looker.com","url":"https://localhost:19999/api/4.0/users/14/credentials_email","user_url":"https://localhost:19999/api/4.0/users/14","password_reset_url":"https://localhost:19999/api/4.0"},"url":"https://localhost:19999/api/4.0/users/14"}
89 |
90 | # Logout to revoke an access_token
91 | > curl -i -X DELETE -H "Authorization: token 4QDkCyCtZzYgj4C2p2cj3csJH7zqS5RzKs2kTnG4" https://localhost:19999/logout
92 | HTTP/1.1 204 No Content
93 | X-Content-Type-Options: nosniff
94 |
95 | # Show that the access_token is no longer valid
96 | > curl -i -X DELETE -H "Authorization: token 4QDkCyCtZzYgj4C2p2cj3csJH7zqS5RzKs2kTnG4" https://localhost:19999/logout
97 | HTTP/1.1 404 Not Found
98 | Content-Type: application/json;charset=utf-8
99 | Vary: Accept-Encoding
100 | X-Content-Type-Options: nosniff
101 | Content-Length: 69
102 |
103 | {"message":"Not found","documentation_url":"http://docs.looker.com/"}
104 | ```
105 |
--------------------------------------------------------------------------------
/test/looker/test_dynamic_client_agent.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | require_relative '../helper'
26 |
27 | describe LookerSDK::Client::Dynamic do
28 |
29 | def access_token
30 | '87614b09dd141c22800f96f11737ade5226d7ba8'
31 | end
32 |
33 | def sdk_client(swagger)
34 | LookerSDK::Client.new do |config|
35 | config.swagger = swagger
36 | config.access_token = access_token
37 | end
38 | end
39 |
40 | def default_swagger
41 | @swagger ||= JSON.parse(File.read(File.join(File.dirname(__FILE__), 'swagger.json')), :symbolize_names => true)
42 | end
43 |
44 | def sdk
45 | @sdk ||= sdk_client(default_swagger)
46 | end
47 |
48 | def with_stub(klass, method, result)
49 | klass.stubs(method).returns(result)
50 | begin
51 | yield
52 | ensure
53 | klass.unstub(method)
54 | end
55 | end
56 |
57 | def response
58 | OpenStruct.new(:data => "foo", :status => 200)
59 | end
60 |
61 | def delete_response
62 | OpenStruct.new(:data => "", :status => 204)
63 | end
64 |
65 | describe "swagger" do
66 | it "get" do
67 | mock = MiniTest::Mock.new.expect(:call, response, [:get, '/api/3.0/user', nil, {}])
68 | with_stub(Sawyer::Agent, :new, mock) do
69 | sdk.me
70 | mock.verify
71 | end
72 | end
73 |
74 | it "get with parms" do
75 | mock = MiniTest::Mock.new.expect(:call, response, [:get, '/api/3.0/users/25', nil, {}])
76 | with_stub(Sawyer::Agent, :new, mock) do
77 | sdk.user(25)
78 | mock.verify
79 | end
80 | end
81 |
82 | it "get with query" do
83 | mock = MiniTest::Mock.new.expect(:call, response, [:get, '/api/3.0/user', nil, {query:{bar:"foo"}}])
84 | with_stub(Sawyer::Agent, :new, mock) do
85 | sdk.me({bar:'foo'})
86 | mock.verify
87 | end
88 | end
89 |
90 | it "get with params and query" do
91 | mock = MiniTest::Mock.new.expect(:call, response, [:get, '/api/3.0/users/25', nil, {query:{bar:"foo"}}])
92 | with_stub(Sawyer::Agent, :new, mock) do
93 | sdk.user(25, {bar:'foo'})
94 | mock.verify
95 | end
96 | end
97 |
98 | it "post" do
99 | mock = MiniTest::Mock.new.expect(:call, response, [:post, '/api/3.0/users', {first_name:'Joe'}, {:headers=>{:content_type=>"application/json"}}])
100 | with_stub(Sawyer::Agent, :new, mock) do
101 | sdk.create_user({first_name:'Joe'})
102 | mock.verify
103 | end
104 | end
105 |
106 | it "post with default body" do
107 | mock = MiniTest::Mock.new.expect(:call, response, [:post, '/api/3.0/users', {}, {:headers=>{:content_type=>"application/json"}}])
108 | with_stub(Sawyer::Agent, :new, mock) do
109 | sdk.create_user()
110 | mock.verify
111 | end
112 | end
113 |
114 | it "patch" do
115 | mock = MiniTest::Mock.new.expect(:call, response, [:patch, '/api/3.0/users/25', {first_name:'Jim'}, {:headers=>{:content_type=>"application/json"}}])
116 | with_stub(Sawyer::Agent, :new, mock) do
117 | sdk.update_user(25, {first_name:'Jim'})
118 | mock.verify
119 | end
120 | end
121 |
122 | it "put" do
123 | mock = MiniTest::Mock.new.expect(:call, response, [:put, '/api/3.0/users/25/roles', [10, 20], {:headers=>{:content_type=>"application/json"}}])
124 | with_stub(Sawyer::Agent, :new, mock) do
125 | sdk.set_user_roles(25, [10,20])
126 | mock.verify
127 | end
128 | end
129 |
130 | it "put with nil body" do
131 | mock = MiniTest::Mock.new.expect(:call, response, [:put, '/api/3.0/users/25/roles', nil, {}])
132 | with_stub(Sawyer::Agent, :new, mock) do
133 | sdk.set_user_roles(25, nil)
134 | mock.verify
135 | end
136 | end
137 |
138 | it "put with empty body" do
139 | mock = MiniTest::Mock.new.expect(:call, response, [:put, '/api/3.0/users/25/roles', {}, {:headers=>{:content_type=>"application/json"}}])
140 | with_stub(Sawyer::Agent, :new, mock) do
141 | sdk.set_user_roles(25, {})
142 | mock.verify
143 | end
144 | end
145 |
146 | it "delete" do
147 | mock = MiniTest::Mock.new.expect(:call, delete_response, [:delete, '/api/3.0/users/25', nil, {}])
148 | with_stub(Sawyer::Agent, :new, mock) do
149 | sdk.delete_user(25)
150 | mock.verify
151 | end
152 | end
153 |
154 | end
155 | end
156 |
--------------------------------------------------------------------------------
/lib/looker-sdk/configurable.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | module LookerSDK
26 |
27 | # Configuration options for {Client}, defaulting to values
28 | # in {Default}
29 | module Configurable
30 | # @!attribute [w] access_token
31 | # @see look TODO docs link
32 | # @return [String] OAuth2 access token for authentication
33 | # @!attribute api_endpoint
34 | # @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"
35 | # @!attribute auto_paginate
36 | # @return [Boolean] Auto fetch next page of results until rate limit reached
37 | # @!attribute client_id
38 | # @see look TODO docs link
39 | # @return [String] Configure OAuth app key
40 | # @!attribute [w] client_secret
41 | # @see look TODO docs link
42 | # @return [String] Configure OAuth app secret
43 | # @!attribute default_media_type
44 | # @see look TODO docs link
45 | # @return [String] Configure preferred media type (for API versioning, for example)
46 | # @!attribute connection_options
47 | # @see https://github.com/lostisland/faraday
48 | # @return [Hash] Configure connection options for Faraday
49 | # @!attribute middleware
50 | # @see https://github.com/lostisland/faraday
51 | # @return [Faraday::Builder or Faraday::RackBuilder] Configure middleware for Faraday
52 | # @!attribute netrc
53 | # @return [Boolean] Instruct Looker to get credentials from .netrc file
54 | # @!attribute netrc_file
55 | # @return [String] Path to .netrc file. default: ~/.netrc
56 | # @!attribute per_page
57 | # @return [String] Configure page size for paginated results. API default: 30
58 | # @!attribute proxy
59 | # @see https://github.com/lostisland/faraday
60 | # @return [String] URI for proxy server
61 | # @!attribute user_agent
62 | # @return [String] Configure User-Agent header for requests.
63 | # @!attribute web_endpoint
64 | # @return [String] Base URL for web URLs. default: https://.looker.com/ look TODO is this correct?
65 |
66 | attr_accessor :access_token, :auto_paginate, :client_id,
67 | :client_secret, :default_media_type, :connection_options,
68 | :middleware, :netrc, :netrc_file,
69 | :per_page, :proxy, :user_agent, :faraday, :swagger, :shared_swagger, :raw_responses,
70 | :lazy_swagger
71 | attr_writer :web_endpoint, :api_endpoint
72 |
73 | class << self
74 |
75 | # List of configurable keys for {LookerSDK::Client}
76 | # @return [Array] of option keys
77 | def keys
78 | @keys ||= [
79 | :access_token,
80 | :api_endpoint,
81 | :auto_paginate,
82 | :client_id,
83 | :client_secret,
84 | :connection_options,
85 | :default_media_type,
86 | :middleware,
87 | :netrc,
88 | :netrc_file,
89 | :per_page,
90 | :proxy,
91 | :user_agent,
92 | :faraday,
93 | :shared_swagger,
94 | :swagger,
95 | :raw_responses,
96 | :web_endpoint,
97 | :lazy_swagger,
98 | ]
99 | end
100 | end
101 |
102 | # Set configuration options using a block
103 | def configure
104 | yield self
105 | end
106 |
107 | # Reset configuration options to default values
108 | def reset!
109 | LookerSDK::Configurable.keys.each do |key|
110 | instance_variable_set(:"@#{key}", LookerSDK::Default.options[key])
111 | end
112 | self
113 | end
114 | alias setup reset!
115 |
116 | def api_endpoint
117 | File.join(@api_endpoint, "")
118 | end
119 |
120 | # Base URL for generated web URLs
121 | #
122 | # @return [String] Default: https://.looker.com/ look TODO is this correct?
123 | def web_endpoint
124 | File.join(@web_endpoint, "")
125 | end
126 |
127 | def netrc?
128 | !!@netrc
129 | end
130 |
131 | private
132 |
133 | def options
134 | Hash[LookerSDK::Configurable.keys.map{|key| [key, instance_variable_get(:"@#{key}")]}]
135 | end
136 |
137 | def fetch_client_id_and_secret(overrides = {})
138 | opts = options.merge(overrides)
139 | opts.values_at :client_id, :client_secret
140 | end
141 | end
142 | end
143 |
--------------------------------------------------------------------------------
/lib/looker-sdk/default.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | require 'looker-sdk/response/raise_error'
26 | require 'looker-sdk/version'
27 |
28 | module LookerSDK
29 |
30 | # Default configuration options for {Client}
31 | module Default
32 |
33 | # Default API endpoint look TODO update this as needed
34 | API_ENDPOINT = "https://localhost:19999/api/3.0/".freeze
35 |
36 | # Default User Agent header string
37 | USER_AGENT = "Looker Ruby Gem #{LookerSDK::VERSION}".freeze
38 |
39 | # Default media type
40 | MEDIA_TYPE = "application/json"
41 |
42 | # Default WEB endpoint
43 | WEB_ENDPOINT = "https://localhost:9999".freeze # look TODO update this
44 |
45 | # In Faraday 0.9, Faraday::Builder was renamed to Faraday::RackBuilder
46 | RACK_BUILDER_CLASS = defined?(Faraday::RackBuilder) ? Faraday::RackBuilder : Faraday::Builder
47 |
48 | # Default Faraday middleware stack
49 | MIDDLEWARE = RACK_BUILDER_CLASS.new do |builder|
50 | builder.use LookerSDK::Response::RaiseError
51 | builder.adapter Faraday.default_adapter
52 | end
53 |
54 | class << self
55 |
56 | # Configuration options
57 | # @return [Hash]
58 | def options
59 | Hash[LookerSDK::Configurable.keys.map{|key| [key, send(key)]}]
60 | end
61 |
62 | # Default access token from ENV
63 | # @return [String]
64 | def access_token
65 | ENV['LOOKER_ACCESS_TOKEN']
66 | end
67 |
68 | # Default API endpoint from ENV or {API_ENDPOINT}
69 | # @return [String]
70 | def api_endpoint
71 | ENV['LOOKER_API_ENDPOINT'] || API_ENDPOINT
72 | end
73 |
74 | # Default pagination preference from ENV
75 | # @return [String]
76 | def auto_paginate
77 | ENV['LOOKER_AUTO_PAGINATE']
78 | end
79 |
80 | # Default OAuth app key from ENV
81 | # @return [String]
82 | def client_id
83 | ENV['LOOKER_CLIENT_ID']
84 | end
85 |
86 | # Default OAuth app secret from ENV
87 | # @return [String]
88 | def client_secret
89 | ENV['LOOKER_SECRET']
90 | end
91 |
92 | # Default options for Faraday::Connection
93 | # @return [Hash]
94 | def connection_options
95 | {
96 | :headers => {
97 | :accept => default_media_type,
98 | :user_agent => user_agent
99 | }
100 | }
101 | end
102 |
103 | # Default media type from ENV or {MEDIA_TYPE}
104 | # @return [String]
105 | def default_media_type
106 | ENV['LOOKER_DEFAULT_MEDIA_TYPE'] || MEDIA_TYPE
107 | end
108 |
109 | # Default middleware stack for Faraday::Connection
110 | # from {MIDDLEWARE}
111 | # @return [String]
112 | def middleware
113 | MIDDLEWARE
114 | end
115 |
116 | def faraday
117 | nil
118 | end
119 |
120 | def swagger
121 | nil
122 | end
123 |
124 | def shared_swagger
125 | false
126 | end
127 |
128 | # Default behavior for loading swagger during initialization or at first call
129 | # @return [Boolean]
130 | def lazy_swagger
131 | false
132 | end
133 |
134 | def raw_responses
135 | false
136 | end
137 |
138 | # Default pagination page size from ENV
139 | # @return [Fixnum] Page size
140 | def per_page
141 | page_size = ENV['LOOKER_PER_PAGE']
142 |
143 | page_size.to_i if page_size
144 | end
145 |
146 | # Default proxy server URI for Faraday connection from ENV
147 | # @return [String]
148 | def proxy
149 | ENV['LOOKER_PROXY']
150 | end
151 |
152 | # Default User-Agent header string from ENV or {USER_AGENT}
153 | # @return [String]
154 | def user_agent
155 | ENV['LOOKER_USER_AGENT'] || USER_AGENT
156 | end
157 |
158 | # Default web endpoint from ENV or {WEB_ENDPOINT}
159 | # @return [String]
160 | def web_endpoint
161 | ENV['LOOKER_WEB_ENDPOINT'] || WEB_ENDPOINT
162 | end
163 |
164 | # Default behavior for reading .netrc file
165 | # @return [Boolean]
166 | def netrc
167 | ENV['LOOKER_NETRC'] || false
168 | end
169 |
170 | # Default path for .netrc file
171 | # @return [String]
172 | def netrc_file
173 | ENV['LOOKER_NETRC_FILE'] || File.join(ENV['HOME'].to_s, '.netrc')
174 | end
175 |
176 | end
177 | end
178 | end
179 |
--------------------------------------------------------------------------------
/lib/looker-sdk/client/dynamic.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | module LookerSDK
26 | class Client
27 |
28 | module Dynamic
29 |
30 | attr_accessor :dynamic
31 |
32 | def try_load_swagger
33 | resp = get('swagger.json') rescue nil
34 | resp && last_response && last_response.status == 200 && last_response.data && last_response.data.to_attrs
35 | end
36 |
37 | # If a given client is created with ':shared_swagger => true' then it will try to
38 | # use a globally sharable @@operations hash built from one fetch of the swagger.json for the
39 | # given api_endpoint. This is an optimization for the cases where many sdk clients get created and
40 | # destroyed (perhaps with different access tokens) while all talking to the same endpoints. This cuts
41 | # down overhead for such cases considerably.
42 |
43 | @@sharable_operations = Hash.new
44 |
45 | def clear_swagger
46 | @swagger = @operations = nil
47 | end
48 |
49 | def load_swagger
50 | # We only need the swagger if we are going to be building our own 'operations' hash
51 | return if shared_swagger && @@sharable_operations[api_endpoint]
52 | # First, try to load swagger.json w/o authenticating
53 | @swagger ||= without_authentication { try_load_swagger }
54 |
55 | unless @swagger
56 | # try again, this time with authentication
57 | @swagger = try_load_swagger
58 | end
59 |
60 | # in unit tests, @swagger may be nil and last_response nil because no HTTP request was made
61 | if @swagger.nil?
62 | if @last_error
63 | raise @last_error
64 | else
65 | raise "Load of swagger.json failed."
66 | end
67 | end
68 |
69 | @swagger
70 | end
71 |
72 | def operations
73 | return @@sharable_operations[api_endpoint] if shared_swagger && @@sharable_operations[api_endpoint]
74 |
75 | if !@swagger && @lazy_swagger
76 | load_swagger
77 | end
78 |
79 | return nil unless @swagger
80 | @operations ||= Hash[
81 | @swagger[:paths].map do |path_name, path_info|
82 | path_info.map do |method, route_info|
83 | route = @swagger[:basePath].to_s + path_name.to_s
84 | [route_info[:operationId].to_sym, {:route => route, :method => method, :info => route_info}]
85 | end
86 | end.reduce(:+)
87 | ].freeze
88 |
89 | shared_swagger ? (@@sharable_operations[api_endpoint] = @operations) : @operations
90 | end
91 |
92 | def method_link(entry)
93 | uri = URI.parse(api_endpoint)
94 | "#{uri.scheme}://#{uri.host}:#{uri.port}/api-docs/index.html#!/#{entry[:info][:tags].first}/#{entry[:info][:operationId]}" rescue "http://docs.looker.com/"
95 | end
96 |
97 | # Callers can explicitly 'invoke' remote methods or let 'method_missing' do the trick.
98 | # If nothing else, this gives clients a way to deal with potential conflicts between remote method
99 | # names and names of methods on client itself.
100 | def invoke(method_name, *args, &block)
101 | entry = find_entry(method_name) || raise(NameError, "undefined remote method '#{method_name}'")
102 | invoke_remote(entry, method_name, *args, &block)
103 | end
104 |
105 | def method_missing(method_name, *args, &block)
106 | entry = find_entry(method_name) || (return super)
107 | invoke_remote(entry, method_name, *args, &block)
108 | end
109 |
110 | def respond_to?(method_name, include_private=false)
111 | !!find_entry(method_name) || super
112 | end
113 |
114 | private
115 |
116 | def find_entry(method_name)
117 | operations && operations[method_name.to_sym] if dynamic
118 | end
119 |
120 | def invoke_remote(entry, method_name, *args, &block)
121 | args = (args || []).dup
122 | route = entry[:route].to_s.dup
123 | params = (entry[:info][:parameters] || []).select {|param| param[:in] == 'path'}
124 | body_param = (entry[:info][:parameters] || []).select {|param| param[:in] == 'body'}.first
125 |
126 | params_passed = args.length
127 | params_required = params.length + (body_param && body_param[:required] ? 1 : 0)
128 | unless params_passed >= params_required
129 | raise ArgumentError.new("wrong number of arguments (#{params_passed} for #{params_required}) in call to '#{method_name}'. See '#{method_link(entry)}'")
130 | end
131 |
132 | # substitute the actual params into the route template, encoding if needed
133 | params.each do |param|
134 | value = args.shift.to_s
135 | if value == CGI.unescape(value)
136 | value = CGI.escape(value)
137 | end
138 | route.sub!("{#{param[:name]}}", value)
139 | end
140 |
141 | a = args.length > 0 ? args[0] : {}
142 | b = args.length > 1 ? args[1] : {}
143 |
144 | method = entry[:method].to_sym
145 | case method
146 | when :get then get(route, a, true, &block)
147 | when :post then post(route, a, merge_content_type_if_body(a, b), true, &block)
148 | when :put then put(route, a, merge_content_type_if_body(a, b), true, &block)
149 | when :patch then patch(route, a, merge_content_type_if_body(a, b), true, &block)
150 | when :delete then delete(route, a, true) ; @raw_responses ? last_response : delete_succeeded?
151 | else raise "unsupported method '#{method}' in call to '#{method_name}'. See '#{method_link(entry)}'"
152 | end
153 | end
154 |
155 | end
156 | end
157 | end
158 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # This Respoitory is Retired
2 | **The new home of this project is `git@github.com:looker-open-source/looker-sdk-ruby.git`**
3 |
4 | Please execute the following commands on your local versions:
5 | ```
6 | git remote set-url origin git@github.com:looker-open-source/looker-sdk-ruby.git
7 | # or git remote set-url origin https://github.com/looker-open-source/looker-sdk-ruby.git
8 | git remote -v
9 | git branch -m master main
10 | git fetch origin
11 | git branch -u origin/main main
12 | git remote set-head origin -a
13 | git remote prune origin
14 | ```
15 |
16 | If you have forked this repository in github, you will likely need to delete that
17 | forked version and fork the new repository.
18 |
19 | **If you have work in your github fork** you can preserve that work by pulling
20 | anything in your forked copy down before deleting the forked copy.
21 |
22 | Suppose your forked copy is listed in your git remotes as `my-looker-sdk-ruby`. Simply
23 | execute the following command...
24 | ```
25 | git fetch my-looker-sdk-ruby
26 | ```
27 |
28 | Now go to your github and delete or even just rename `my-looker-sdk-ruby`. Fork the version
29 | at https://github.com/looker-open-source/looker-sdk-ruby and name that with `my-looker-sdk-ruby`.
30 | The `master` branch has been renamed `main` to keep with modern naming standards. On your local
31 | version execute the following to rename your local `master` branch and push to `main` on your ...
32 | ```
33 | git fetch my-looker-sdk-ruby
34 | git branch -m master main
35 | git checkout main
36 | git rebase my-looker-sdk-ruby/main
37 | git push my-looker-sdk-ruby main
38 | export MY_REMOTE="my-looker-sdk-ruby"
39 | git branch -r | cut -c 3- | \
40 | grep -E "^${MY_REMOTE}/.+" | \
41 | cut -d / -f 2- | \
42 | xargs -L 1 -I {} git push --follow-tags ${MY_REMOTE} refs/remotes/${MY_REMOTE}/{}:refs/heads/{}
43 | ```
44 |
45 | Now any work that was on your old fork should be on your new fork.
46 |
47 |
48 | # [Looker](http://looker.com/) SDK for Ruby [](https://travis-ci.org/looker/looker-sdk-ruby)
49 | ### Overview
50 | This SDK supports secure/authenticated access to the Looker RESTful API. The SDK binds dynamically to the Looker API and builds mappings for the sets of API methods that the Looker instance exposes. This allows for writing straightforward Ruby scripts to interact with the Looker API. And, it allows the SDK to provide access to new Looker API features in each Looker release without requiring an update to the SDK each time.
51 |
52 | The Looker API uses OAuth2 authentication. 'API3' keys can be generated by Looker admins for any Looker user account from the Looker admin panel. These 'keys' each consist of a client_id/client_secret pair. These keys should be carefully protected as one would with any critical password. When using the SDK, one creates a client object that is initialized with a client_id/client_secret pair and the base URL of the Looker instance's API endpoint. The SDK transparently logs in to the API with that key pair to generate a short-term auth token that it sends to the API with each subsequent call to provide authentication for that call.
53 |
54 | All calls to the Looker API must be done over a TLS/SSL connection. Requests and responses are then encrypted at that transport layer. It is highly recommended that Looker instance https endpoints use certificates that are properly signed by a trusted certificate authority. The SDK will, by default, validate server certificates. It is possible to disable that validation when creating an SDK client object if necessary. But, that configuration is discouraged.
55 |
56 | Looker instances expose API documentation at: https://mygreatcompany.looker.com:19999/api-docs/index.html (the exact URL can be set in the Looker admin panel). By default, the documentation page requires a client_id/client_secret pair to load the detailed API information. That page also supports "Try it out!" links so that you can experiment with the API right from the documentation. The documentation is intended to show how to call the API endpoints via either raw RESTful https requests or using the SDK.
57 |
58 | Keep in mind that all API calls are done 'as' the user whose credentials were used to login to the API. The Looker permissioning system enforces various rules about which activities users with various permissions are and are not allowed to do; and data they are or are not allowed to access. For instance, there are many configuration and looker management activities that only Admin users are allowed to perform; like creating and asigning user roles. Additionally, non-admin users have very limited access to information about other users.
59 |
60 | When trying to access a resource with the API that the current user is not allowed to access, the API will return a '404 Not Found' error - the same as if the resource did not exist at all. This is a standard practice for RESTful services. By default, the Ruby SDK will convert all non-success result codes into ruby exceptions which it then raises. So, error paths are handled by rescuing exceptions rather than checking result codes for each SDK request.
61 |
62 | ### Installation
63 | ```bash
64 | $ git clone git@github.com:looker/looker-sdk-ruby.git looker-sdk
65 | $ cd looker-sdk
66 | $ gem install bundle
67 | $ bundle install
68 | $ rake install
69 | ```
70 |
71 | ### Development
72 |
73 | Rename test/fixtures/.netrc.template to test/fixtures/.netrc and add API3
74 | credentials for tests to pass.
75 | Comment out coverage configuration in test/helper.rb for debugging.
76 | ```bash
77 | $ bundle install
78 | $ rake test # run the test suite
79 | $ make install test # run the test suite on all supported Rubies
80 | ```
81 |
82 | ### Basic Usage
83 |
84 | ```ruby
85 | require 'looker-sdk'
86 |
87 | # An sdk client can be created with an explicit client_id/client_secret pair
88 | # (this is discouraged because secrets in code files can easily lead to those secrets being compromised!)
89 | sdk = LookerSDK::Client.new(
90 | :client_id => "4CN7jzm7yrkcy2MC4CCG",
91 | :client_secret => "Js3rZZ7vHfbc2hBynSj7zqKh",
92 | :api_endpoint => "https://mygreatcompany.looker.com:19999/api/4.0"
93 | )
94 |
95 | # If you don't want to provide explicit credentials: (trust me you don't)
96 | # add the below to your ~/.netrc file (or create the file if you don't have one).
97 | # Note that to use netrc you need to install the netrc ruby gem.
98 | #
99 | # machine mygreatcompany.looker.com
100 | # login my_client_id
101 | # password my_client_secret
102 |
103 | sdk = LookerSDK::Client.new(
104 | :netrc => true,
105 | :netrc_file => "~/.net_rc",
106 | :api_endpoint => "https://mygreatcompany.looker.com:19999/api/4.0",
107 |
108 | # Set longer timeout to allow for long running queries. The default is 60 seconds and can be problematic.
109 | :connection_options => {:request => {:timeout => 60 * 60, :open_timeout => 30}},
110 |
111 | # Alternately, disable cert verification if the looker has a self-signed cert.
112 | # Avoid this if using real certificates; verification of the server cert is a very good thing for production.
113 | # :connection_options => {:ssl => {:verify => false}},
114 |
115 | # Alternately, support self-signed cert *and* set longer timeout to allow for long running queries.
116 | # :connection_options => {:ssl => {:verify => false}, :request => {:timeout => 60 * 60, :open_timeout => 30}},
117 | )
118 |
119 | # Check if we can even communicate with the Looker - without even trying to authenticate.
120 | # This will throw an exception if the sdk can't connect at all. This can help a lot with debugging your
121 | # first attempts at using the sdk.
122 | sdk.alive
123 |
124 | # Supports user creation, modification, deletion
125 | # Supports email_credentials creation, modification, and deletion.
126 |
127 | first_user = sdk.create_user({:first_name => "Jonathan", :last_name => "Swenson"})
128 | sdk.create_user_credentials_email(first_user[:id], {:email => "jonathan@example.com"})
129 |
130 | second_user = sdk.create_user({:first_name => "John F", :last_name => "Kennedy"})
131 | sdk.create_user_credentials_email(second_user[:id], {:email => "john@example.com"})
132 |
133 | third_user = sdk.create_user({:first_name => "Frank", :last_name => "Sinatra"})
134 | sdk.create_user_credentials_email(third_user[:id], {:email => "frank@example.com"})
135 |
136 | user = sdk.user(first_user[:id])
137 | user.first_name # Jonathan
138 | user.last_name # Swenson
139 |
140 | sdk.update_user(first_user[:id], {:first_name => "Jonathan is awesome"})
141 | user = sdk.user(first_user[:id])
142 | user.first_name # "Jonathan is awesome"
143 |
144 | credentials_email = sdk.user_credentials_email(user[:id])
145 | credentials_email[:email] # jonathan@example.com
146 |
147 | sdk.update_user_credentials_email(user[:id], {:email => "jonathan+1@example.com"})
148 | credentials_email = sdk.user_credentials_email(user[:id])
149 | credentials_email[:email] # jonathan+1@example.com
150 |
151 | users = sdk.all_users()
152 | users.length # 3
153 | users[0] # first_user
154 |
155 |
156 | sdk.delete_user_credentials_email(second_user[:id])
157 | sdk.delete_user(second_user[:id])
158 |
159 | users = sdk.all_users()
160 | users.length # 2
161 | users[1] # third_user
162 |
163 | ```
164 |
165 | ### TODO
166 | Things that we think are important to do will be marked with `look TODO`
167 |
168 |
--------------------------------------------------------------------------------
/test/looker/test_dynamic_client.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | require_relative '../helper'
26 |
27 | class LookerDynamicClientTest < MiniTest::Spec
28 |
29 | def access_token
30 | '87614b09dd141c22800f96f11737ade5226d7ba8'
31 | end
32 |
33 | def sdk_client(swagger, engine)
34 | faraday = Faraday.new do |conn|
35 | conn.use LookerSDK::Response::RaiseError
36 | conn.adapter :rack, engine
37 | end
38 |
39 | LookerSDK.reset!
40 | LookerSDK::Client.new do |config|
41 | config.swagger = swagger
42 | config.access_token = access_token
43 | config.faraday = faraday
44 | end
45 | end
46 |
47 | def default_swagger
48 | @swagger ||= JSON.parse(File.read(File.join(File.dirname(__FILE__), 'swagger.json')), :symbolize_names => true)
49 | end
50 |
51 | def response
52 | [200, {'Content-Type' => 'application/json'}, [{}.to_json]]
53 | end
54 |
55 | def delete_response
56 | [204, {}, []]
57 | end
58 |
59 | def confirm_env(env, method, path, body, query, content_type)
60 | req = Rack::Request.new(env)
61 | req_body = req.body.gets || ''
62 |
63 | req.base_url.must_equal 'https://localhost:19999'
64 | req.request_method.must_equal method.to_s.upcase
65 | req.path_info.must_equal path
66 |
67 | env["HTTP_AUTHORIZATION"].must_equal "token #{access_token}"
68 |
69 | JSON.parse(req.params.to_json, :symbolize_names => true).must_equal query
70 |
71 | begin
72 | JSON.parse(req_body, :symbolize_names => true).must_equal body
73 | rescue JSON::ParserError => e
74 | req_body.must_equal body
75 | end
76 |
77 | req.content_type.must_equal(content_type) if content_type
78 |
79 | # puts env
80 | # puts req.inspect
81 | # puts req.params.inspect
82 | # puts req_body
83 | # puts req.content_type
84 |
85 | true
86 | end
87 |
88 | def verify(response, method, path, body='', query={}, content_type = nil)
89 | mock = MiniTest::Mock.new.expect(:call, response){|env| confirm_env(env, method, path, body, query, content_type)}
90 | yield sdk_client(default_swagger, mock)
91 | mock.verify
92 | end
93 |
94 | describe "swagger" do
95 |
96 | it "raises when swagger.json can't be loaded" do
97 | mock = MiniTest::Mock.new.expect(:call, nil) {raise "no swagger for you"}
98 | mock.expect(:call, nil) {raise "still no swagger for you"}
99 | err = assert_raises(RuntimeError) { sdk_client(nil, mock) }
100 | assert_equal "still no swagger for you", err.message
101 | end
102 |
103 | it "loads swagger without authentication" do
104 | resp = [200, {'Content-Type' => 'application/json'}, [default_swagger.to_json]]
105 | mock = MiniTest::Mock.new.expect(:call, resp, [Hash])
106 | sdk = sdk_client(nil, mock)
107 | assert_equal default_swagger, sdk.swagger
108 | end
109 |
110 | it "loads swagger with authentication" do
111 | resp = [200, {'Content-Type' => 'application/json'}, [default_swagger.to_json]]
112 | mock = MiniTest::Mock.new.expect(:call, nil) {raise "login first!"}
113 | mock.expect(:call, resp, [Hash])
114 | sdk = sdk_client(nil, mock)
115 | assert_equal default_swagger, sdk.swagger
116 | end
117 |
118 | it "invalid method name" do
119 | sdk = sdk_client(default_swagger, nil)
120 | assert_raises NoMethodError do
121 | sdk.this_method_name_doesnt_exist()
122 | end
123 |
124 | assert_raises NameError do
125 | sdk.invoke(:this_method_name_doesnt_exist)
126 | end
127 | end
128 |
129 | describe "operation maps" do
130 | it "invoke by string operationId" do
131 | verify(response, :get, '/api/3.0/user') do |sdk|
132 | sdk.invoke('me')
133 | end
134 | end
135 |
136 | it "invoke by symbol operationId" do
137 | verify(response, :get, '/api/3.0/user') do |sdk|
138 | sdk.invoke(:me)
139 | end
140 | end
141 | end
142 |
143 | it "get no params" do
144 | verify(response, :get, '/api/3.0/user') do |sdk|
145 | sdk.me
146 | end
147 | end
148 |
149 | it "get with params" do
150 | verify(response, :get, '/api/3.0/users/25') do |sdk|
151 | sdk.user(25)
152 | end
153 | end
154 |
155 | it "get with params that need encoding" do
156 | verify(response, :get, '/api/3.0/users/foo%2Fbar') do |sdk|
157 | sdk.user("foo/bar")
158 | end
159 | end
160 |
161 | it "get with params already encoded" do
162 | verify(response, :get, '/api/3.0/users/foo%2Fbar') do |sdk|
163 | sdk.user("foo%2Fbar")
164 | end
165 | end
166 |
167 | it "get with query" do
168 | verify(response, :get, '/api/3.0/user', '', {bar:"foo"}) do |sdk|
169 | sdk.me({bar:'foo'})
170 | end
171 | end
172 |
173 | it "get with params and query" do
174 | verify(response, :get, '/api/3.0/users/25', '', {bar:"foo"}) do |sdk|
175 | sdk.user(25, {bar:'foo'})
176 | end
177 | end
178 |
179 | it "get with array query param - string input (csv)" do
180 | verify(response, :get, '/api/3.0/users/1/attribute_values','',{user_attribute_ids: '2,3,4'}) do |sdk|
181 | sdk.user_attribute_user_values(1, {user_attribute_ids: '2,3,4'})
182 | sdk.last_response.env.url.query.must_equal 'user_attribute_ids=2%2C3%2C4'
183 | end
184 | end
185 |
186 | it "get with array query param - array input (multi[])" do
187 | verify(response, :get, '/api/3.0/users/1/attribute_values','',{user_attribute_ids: ['2','3','4']}) do |sdk|
188 | sdk.user_attribute_user_values(1, {user_attribute_ids: [2,3,4]})
189 | sdk.last_response.env.url.query.must_equal 'user_attribute_ids%5B%5D=2&user_attribute_ids%5B%5D=3&user_attribute_ids%5B%5D=4'
190 | end
191 | end
192 |
193 | it "post" do
194 | verify(response, :post, '/api/3.0/users', {first_name:'Joe'}) do |sdk|
195 | sdk.create_user({first_name:'Joe'})
196 | end
197 | end
198 |
199 | it "post with default body" do
200 | verify(response, :post, '/api/3.0/users', {}) do |sdk|
201 | sdk.create_user()
202 | end
203 | end
204 |
205 | it "post with default body and default content_type" do
206 | verify(response, :post, '/api/3.0/users', {}, {}, "application/json") do |sdk|
207 | sdk.create_user()
208 | end
209 | end
210 |
211 | it "post with default body and specific content_type at option-level" do
212 | verify(response, :post, '/api/3.0/users', {}, {}, "application/vnd.BOGUS1+json") do |sdk|
213 | sdk.create_user({}, {:content_type => "application/vnd.BOGUS1+json"})
214 | end
215 | end
216 |
217 | it "post with default body and specific content_type in headers" do
218 | verify(response, :post, '/api/3.0/users', {}, {}, "application/vnd.BOGUS2+json") do |sdk|
219 | sdk.create_user({}, {:headers => {:content_type => "application/vnd.BOGUS2+json"}})
220 | end
221 | end
222 |
223 | it "post with file upload" do
224 | verify(response, :post, '/api/3.0/users', {first_name:'Joe', last_name:'User'}, {}, "application/vnd.BOGUS3+json") do |sdk|
225 | name = File.join(File.dirname(__FILE__), 'user.json')
226 | sdk.create_user(Faraday::UploadIO.new(name, "application/vnd.BOGUS3+json"))
227 | end
228 | end
229 |
230 | it "patch" do
231 | verify(response, :patch, '/api/3.0/users/25', {first_name:'Jim'}) do |sdk|
232 | sdk.update_user(25, {first_name:'Jim'})
233 | end
234 | end
235 |
236 | it "patch with query" do
237 | verify(response, :post, '/api/3.0/users', {first_name:'Jim'}, {foo:'bar', baz:'bla'}) do |sdk|
238 | sdk.create_user({first_name:'Jim'}, {foo:'bar', baz:'bla'})
239 | end
240 | end
241 |
242 | it "put" do
243 | verify(response, :put, '/api/3.0/users/25/roles', [10, 20]) do |sdk|
244 | sdk.set_user_roles(25, [10,20])
245 | end
246 | end
247 |
248 | it "delete" do
249 | verify(delete_response, :delete, '/api/3.0/users/25') do |sdk|
250 | sdk.delete_user(25)
251 | end
252 | end
253 |
254 | end
255 | end
256 |
--------------------------------------------------------------------------------
/lib/looker-sdk/error.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | module LookerSDK
26 | class Error < StandardError
27 |
28 | # Returns the appropriate LookerSDK::Error sublcass based
29 | # on status and response message
30 | #
31 | # @param [Hash] response HTTP response
32 | # @return [LookerSDK::Error]
33 | def self.from_response(response)
34 | status = response[:status].to_i
35 | body = response[:body].to_s
36 | headers = response[:response_headers]
37 |
38 | if klass = case status
39 | when 400 then LookerSDK::BadRequest
40 | when 401 then error_for_401(headers)
41 | when 403 then error_for_403(body)
42 | when 404 then LookerSDK::NotFound
43 | when 405 then LookerSDK::MethodNotAllowed
44 | when 406 then LookerSDK::NotAcceptable
45 | when 409 then LookerSDK::Conflict
46 | when 415 then LookerSDK::UnsupportedMediaType
47 | when 422 then LookerSDK::UnprocessableEntity
48 | when 429 then LookerSDK::RateLimitExceeded
49 | when 400..499 then LookerSDK::ClientError
50 | when 500 then LookerSDK::InternalServerError
51 | when 501 then LookerSDK::NotImplemented
52 | when 502 then LookerSDK::BadGateway
53 | when 503 then LookerSDK::ServiceUnavailable
54 | when 500..599 then LookerSDK::ServerError
55 | end
56 | klass.new(response)
57 | end
58 | end
59 |
60 | def initialize(response=nil)
61 | @response = response
62 | super(build_error_message)
63 | end
64 |
65 | # Documentation URL returned by the API for some errors
66 | #
67 | # @return [String]
68 | def documentation_url
69 | data[:documentation_url] if data.is_a? Hash
70 | end
71 |
72 | # Message string returned by the API for some errors
73 | #
74 | # @return [String]
75 | def message
76 | response_message
77 | end
78 |
79 | # Returns most appropriate error for 401 HTTP status code
80 | # @private
81 | def self.error_for_401(headers)
82 | if LookerSDK::OneTimePasswordRequired.required_header(headers)
83 | LookerSDK::OneTimePasswordRequired
84 | else
85 | LookerSDK::Unauthorized
86 | end
87 | end
88 |
89 | # Returns most appropriate error for 403 HTTP status code
90 | # @private
91 | def self.error_for_403(body)
92 | if body =~ /rate limit exceeded/i
93 | LookerSDK::TooManyRequests
94 | elsif body =~ /login attempts exceeded/i
95 | LookerSDK::TooManyLoginAttempts
96 | else
97 | LookerSDK::Forbidden
98 | end
99 | end
100 |
101 | # Array of validation errors
102 | # @return [Array] Error info
103 | def errors
104 | if data && data.is_a?(Hash)
105 | data[:errors] || []
106 | else
107 | []
108 | end
109 | end
110 |
111 | private
112 |
113 | def data
114 | @data ||=
115 | if (body = @response[:body]) && !body.empty?
116 | if body.is_a?(String) &&
117 | @response[:response_headers] &&
118 | @response[:response_headers][:content_type] =~ /json/
119 |
120 | Sawyer::Agent.serializer.decode(body)
121 | else
122 | body
123 | end
124 | else
125 | nil
126 | end
127 | end
128 |
129 | def response_message
130 | case data
131 | when Hash
132 | data[:message]
133 | when String
134 | data
135 | end
136 | end
137 |
138 | def response_error
139 | "Error: #{data[:error]}" if data.is_a?(Hash) && data[:error]
140 | end
141 |
142 | def response_error_summary
143 | return nil unless data.is_a?(Hash) && !Array(data[:errors]).empty?
144 |
145 | summary = "\nError summary:\n"
146 | summary << data[:errors].map do |hash|
147 | hash.map { |k,v| " #{k}: #{v}" }
148 | end.join("\n")
149 |
150 | summary
151 | end
152 |
153 | def build_error_message
154 | return nil if @response.nil?
155 |
156 | message = "#{@response[:method].to_s.upcase} "
157 | message << redact_url(@response[:url].to_s) + ": "
158 | message << "#{@response[:status]} - "
159 | message << "#{response_message}" unless response_message.nil?
160 | message << "#{response_error}" unless response_error.nil?
161 | message << "#{response_error_summary}" unless response_error_summary.nil?
162 | message << " // See: #{documentation_url}" unless documentation_url.nil?
163 | message
164 | end
165 |
166 | def redact_url(url_string)
167 | %w[client_secret access_token].each do |token|
168 | url_string.gsub!(/#{token}=\S+/, "#{token}=(redacted)") if url_string.include? token
169 | end
170 | url_string
171 | end
172 | end
173 |
174 | # Raised on errors in the 400-499 range
175 | class ClientError < Error; end
176 |
177 | # Raised when API returns a 400 HTTP status code
178 | class BadRequest < ClientError; end
179 |
180 | # Raised when API returns a 401 HTTP status code
181 | class Unauthorized < ClientError; end
182 |
183 | # Raised when API returns a 401 HTTP status code
184 | # and headers include "X-Looker-OTP" look TODO do we want to support this?
185 | class OneTimePasswordRequired < ClientError
186 | #@private
187 | OTP_DELIVERY_PATTERN = /required; (\w+)/i
188 |
189 | #@private
190 | def self.required_header(headers)
191 | OTP_DELIVERY_PATTERN.match headers['X-Looker-OTP'].to_s
192 | end
193 |
194 | # Delivery method for the user's OTP
195 | #
196 | # @return [String]
197 | def password_delivery
198 | @password_delivery ||= delivery_method_from_header
199 | end
200 |
201 | private
202 |
203 | def delivery_method_from_header
204 | if match = self.class.required_header(@response[:response_headers])
205 | match[1]
206 | end
207 | end
208 | end
209 |
210 | # Raised when Looker returns a 403 HTTP status code
211 | class Forbidden < ClientError; end
212 |
213 | # Raised when Looker returns a 403 HTTP status code
214 | # and body matches 'rate limit exceeded'
215 | class TooManyRequests < Forbidden; end
216 |
217 | # Raised when Looker returns a 403 HTTP status code
218 | # and body matches 'login attempts exceeded'
219 | class TooManyLoginAttempts < Forbidden; end
220 |
221 | # Raised when Looker returns a 404 HTTP status code
222 | class NotFound < ClientError; end
223 |
224 | # Raised when Looker returns a 405 HTTP status code
225 | class MethodNotAllowed < ClientError; end
226 |
227 | # Raised when Looker returns a 406 HTTP status code
228 | class NotAcceptable < ClientError; end
229 |
230 | # Raised when Looker returns a 409 HTTP status code
231 | class Conflict < ClientError; end
232 |
233 | # Raised when Looker returns a 414 HTTP status code
234 | class UnsupportedMediaType < ClientError; end
235 |
236 | # Raised when Looker returns a 422 HTTP status code
237 | class UnprocessableEntity < ClientError; end
238 |
239 | # Raised when Looker returns a 429 HTTP status code
240 | class RateLimitExceeded < ClientError; end
241 |
242 | # Raised on errors in the 500-599 range
243 | class ServerError < Error; end
244 |
245 | # Raised when Looker returns a 500 HTTP status code
246 | class InternalServerError < ServerError; end
247 |
248 | # Raised when Looker returns a 501 HTTP status code
249 | class NotImplemented < ServerError; end
250 |
251 | # Raised when Looker returns a 502 HTTP status code
252 | class BadGateway < ServerError; end
253 |
254 | # Raised when Looker returns a 503 HTTP status code
255 | class ServiceUnavailable < ServerError; end
256 |
257 | # Raised when client fails to provide valid Content-Type
258 | class MissingContentType < ArgumentError; end
259 |
260 | # Raised when a method requires an application client_id
261 | # and secret but none is provided
262 | class ApplicationCredentialsRequired < StandardError; end
263 | end
264 |
--------------------------------------------------------------------------------
/test/looker/test_client.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | require_relative '../helper'
26 |
27 | describe LookerSDK::Client do
28 |
29 | before(:each) do
30 | setup_sdk
31 | end
32 |
33 | describe "lazy load swagger" do
34 |
35 | it "lazy loads swagger" do
36 | LookerSDK.reset!
37 | client = LookerSDK::Client.new(
38 | :lazy_swagger => true,
39 | :netrc => true,
40 | :netrc_file => File.join(fixture_path, '.netrc'),
41 | :connection_options => {:ssl => {:verify => false}},
42 | )
43 | assert_nil client.swagger
44 | client.me()
45 | assert client.swagger
46 | end
47 |
48 | it "loads swagger initially" do
49 | LookerSDK.reset!
50 | client = LookerSDK::Client.new(
51 | :lazy_swagger => false,
52 | :netrc => true,
53 | :netrc_file => File.join(fixture_path, '.netrc'),
54 | :connection_options => {:ssl => {:verify => false}},
55 | )
56 | assert client.swagger
57 | end
58 | end
59 |
60 | describe "module configuration" do
61 |
62 | before do
63 | LookerSDK.reset!
64 | LookerSDK.configure do |config|
65 | LookerSDK::Configurable.keys.each do |key|
66 | config.send("#{key}=", "Some #{key}")
67 | end
68 | end
69 | end
70 |
71 | after do
72 | LookerSDK.reset!
73 | end
74 |
75 | it "inherits the module configuration" do
76 | client = LookerSDK::Client.new(:lazy_swagger => true)
77 | LookerSDK::Configurable.keys.each do |key|
78 | next if key == :lazy_swagger
79 | client.instance_variable_get(:"@#{key}").must_equal("Some #{key}")
80 | end
81 | end
82 |
83 | describe "with class level configuration" do
84 |
85 | before do
86 | @opts = {
87 | :connection_options => {:ssl => {:verify => false}},
88 | :per_page => 40,
89 | :client_id => "looker_client_id",
90 | :client_secret => "client_secret2",
91 | :lazy_swagger => true,
92 | }
93 | end
94 |
95 | it "overrides module configuration" do
96 | client = LookerSDK::Client.new(@opts)
97 | client.per_page.must_equal(40)
98 | client.client_id.must_equal("looker_client_id")
99 | client.instance_variable_get(:"@client_secret").must_equal("client_secret2")
100 | client.auto_paginate.must_equal(LookerSDK.auto_paginate)
101 | client.client_id.wont_equal(LookerSDK.client_id)
102 | end
103 |
104 | it "can set configuration after initialization" do
105 | client = LookerSDK::Client.new
106 | client.configure do |config|
107 | @opts.each do |key, value|
108 | config.send("#{key}=", value)
109 | end
110 | end
111 | client.per_page.must_equal(40)
112 | client.client_id.must_equal("looker_client_id")
113 | client.instance_variable_get(:"@client_secret").must_equal("client_secret2")
114 | client.auto_paginate.must_equal(LookerSDK.auto_paginate)
115 | client.client_id.wont_equal(LookerSDK.client_id)
116 | end
117 |
118 | it "masks client_secrets on inspect" do
119 | client = LookerSDK::Client.new(@opts)
120 | inspected = client.inspect
121 | inspected.wont_include("client_secret2")
122 | end
123 |
124 | it "masks tokens on inspect" do
125 | client = LookerSDK::Client.new(:access_token => '87614b09dd141c22800f96f11737ade5226d7ba8')
126 | inspected = client.inspect
127 | inspected.wont_equal("87614b09dd141c22800f96f11737ade5226d7ba8")
128 | end
129 |
130 | it "masks client secrets on inspect" do
131 | client = LookerSDK::Client.new(:client_secret => '87614b09dd141c22800f96f11737ade5226d7ba8')
132 | inspected = client.inspect
133 | inspected.wont_equal("87614b09dd141c22800f96f11737ade5226d7ba8")
134 | end
135 |
136 | describe "with .netrc" do
137 | it "can read .netrc files" do
138 | LookerSDK.reset!
139 | client = LookerSDK::Client.new(
140 | :lazy_swagger => true,
141 | :netrc => true,
142 | :netrc_file => File.join(fixture_path, '.netrc'),
143 | )
144 | client.client_id.wont_be_nil
145 | client.client_secret.wont_be_nil
146 | end
147 | end
148 | end
149 |
150 | describe "config tests" do
151 |
152 | before do
153 | LookerSDK.reset!
154 | LookerSDK.configure do |c|
155 | c.lazy_swagger = true
156 | end
157 | end
158 |
159 | it "sets oauth token with .configure" do
160 | client = LookerSDK::Client.new
161 | client.configure do |config|
162 | config.access_token = 'd255197b4937b385eb63d1f4677e3ffee61fbaea'
163 | end
164 | client.application_credentials?.must_equal false
165 | client.token_authenticated?.must_equal true
166 | end
167 |
168 | it "sets oauth token with initializer block" do
169 | client = LookerSDK::Client.new do |config|
170 | config.access_token = 'd255197b4937b385eb63d1f4677e3ffee61fbaea'
171 | end
172 | client.application_credentials?.must_equal false
173 | client.token_authenticated?.must_equal true
174 | end
175 |
176 | it "sets oauth token with instance methods" do
177 | client = LookerSDK::Client.new
178 | client.access_token = 'd255197b4937b385eb63d1f4677e3ffee61fbaea'
179 | client.application_credentials?.must_equal false
180 | client.token_authenticated?.must_equal true
181 | end
182 |
183 | it "sets oauth application creds with .configure" do
184 | client = LookerSDK::Client.new
185 | client.configure do |config|
186 | config.client_id = '97b4937b385eb63d1f46'
187 | config.client_secret = 'd255197b4937b385eb63d1f4677e3ffee61fbaea'
188 | end
189 | client.application_credentials?.must_equal true
190 | client.token_authenticated?.must_equal false
191 | end
192 |
193 | it "sets oauth application creds with initializer block" do
194 | client = LookerSDK::Client.new do |config|
195 | config.client_id = '97b4937b385eb63d1f46'
196 | config.client_secret = 'd255197b4937b385eb63d1f4677e3ffee61fbaea'
197 | end
198 | client.application_credentials?.must_equal true
199 | client.token_authenticated?.must_equal false
200 | end
201 |
202 | it "sets oauth token with module methods" do
203 | client = LookerSDK::Client.new
204 | client.client_id = '97b4937b385eb63d1f46'
205 | client.client_secret = 'd255197b4937b385eb63d1f4677e3ffee61fbaea'
206 | client.application_credentials?.must_equal true
207 | client.token_authenticated?.must_equal false
208 | end
209 | end
210 |
211 | end
212 |
213 | describe "request options" do
214 |
215 | it "parse_query_and_convenience_headers must handle good input" do
216 |
217 | [
218 | # no need for empty query or headers
219 | [{}, {}],
220 | [{query: {}}, {}],
221 | [{headers: {}}, {}],
222 | [{query: {}, headers: {}}, {}],
223 |
224 | # promote raw stuff into query
225 | [{query:{foo:'bar'}}, {query:{foo:'bar'}}],
226 | [{foo:'bar'}, {query:{foo:'bar'}}],
227 | [{foo:'bar', one:1}, {query:{foo:'bar', one:1}}],
228 |
229 | # promote CONVENIENCE_HEADERS into headers
230 | [{accept: 'foo'}, {headers:{accept: 'foo'}}],
231 | [{content_type: 'foo'}, {headers:{content_type: 'foo'}}],
232 | [{accept: 'foo', content_type: 'bar'}, {headers:{accept: 'foo', content_type: 'bar'}}],
233 |
234 | # merge CONVENIENCE_HEADERS into headers if headers not empty
235 | [{accept: 'foo', headers:{content_type: 'bar'}}, {headers:{accept: 'foo', content_type: 'bar'}}],
236 |
237 | # promote CONVENIENCE_HEADERS into headers while also handling query parts
238 | [{accept: 'foo', content_type: 'bar', query:{foo:'bar'}}, {query:{foo:'bar'}, headers:{accept: 'foo', content_type: 'bar'}}],
239 | [{accept: 'foo', content_type: 'bar', foo:'bar'}, {query:{foo:'bar'}, headers:{accept: 'foo', content_type: 'bar'}}],
240 |
241 | ].each do |pair|
242 | input_original, expected = pair
243 | input = input_original.dup
244 |
245 | output = LookerSDK::Client.new.send(:parse_query_and_convenience_headers, input)
246 |
247 | input.must_equal input_original
248 | output.must_equal expected
249 | end
250 |
251 | # don't make the code above handle the special case of nil input.
252 | LookerSDK::Client.new.send(:parse_query_and_convenience_headers, nil).must_equal({})
253 | end
254 |
255 | it "parse_query_and_convenience_headers must detect bad input" do
256 | [
257 | 1,
258 | '',
259 | [],
260 | {query:1},
261 | {query:[]},
262 | ].each do |input|
263 | proc { LookerSDK::Client.new.send(:parse_query_and_convenience_headers, input) }.must_raise RuntimeError
264 | end
265 | end
266 |
267 | [
268 | [:get, '/api/3.0/users/foo%2Fbar', false],
269 | [:get, '/api/3.0/users/foo%252Fbar', true],
270 | [:post, '/api/3.0/users/foo%2Fbar', false],
271 | [:post, '/api/3.0/users/foo%252Fbar', true],
272 | [:put, '/api/3.0/users/foo%2Fbar', false],
273 | [:put, '/api/3.0/users/foo%252Fbar', true],
274 | [:patch, '/api/3.0/users/foo%2Fbar', false],
275 | [:patch, '/api/3.0/users/foo%252Fbar', true],
276 | [:delete, '/api/3.0/users/foo%2Fbar', false],
277 | [:delete, '/api/3.0/users/foo%252Fbar', true],
278 | [:head, '/api/3.0/users/foo%2Fbar', false],
279 | [:head, '/api/3.0/users/foo%252Fbar', true],
280 | ].each do |method, path, encoded|
281 | it "handles request path encoding" do
282 | expected_path = '/api/3.0/users/foo%252Fbar'
283 |
284 | resp = OpenStruct.new(:data => "hi", :status => 204)
285 | mock = MiniTest::Mock.new.expect(:call, resp, [method, expected_path, nil, {}])
286 | Sawyer::Agent.stubs(:new).returns(mock, mock)
287 |
288 | sdk = LookerSDK::Client.new
289 | if [:get, :delete, :head].include? method
290 | args = [method, path, nil, encoded]
291 | else
292 | args = [method, path, nil, nil, encoded]
293 | end
294 | sdk.without_authentication do
295 | value = sdk.public_send *args
296 | assert_equal "hi", value
297 | end
298 | mock.verify
299 | end
300 | end
301 | end
302 |
303 | describe 'Sawyer date/time parsing patch' do
304 | describe 'key matches time_field pattern' do
305 | it 'does not modify non-iso date/time string or integer fields' do
306 | values = {
307 | :test_at => '30 days',
308 | :test_on => 'July 20, 1969',
309 | :test_date => '1968-04-03 12:23:34', # this is not iso8601 format!
310 | :date => '2 months ago',
311 | :test_int_at => 42,
312 | :test_int_on => 42,
313 | :test_int_date => 42.1,
314 | :test_float_at => 42.1,
315 | :test_float_on => 42.1,
316 | :test_float_date => 42.1,
317 | }
318 |
319 | serializer = LookerSDK::Client.new.send(:serializer)
320 | values.each {|k,v| serializer.decode_hash_value(k,v).must_equal v, k}
321 | end
322 |
323 | it 'converts iso date/time strings to Ruby date/time' do
324 | iso_values = {
325 | :test_at => '2017-02-07T13:21:50-08:00',
326 | :test_on => '2017-02-07T00:00:00z',
327 | :test_date => '1969-07-20T00:00:00-08:00',
328 | :date => '1968-04-03T12:23:34z',
329 | }
330 | serializer = LookerSDK::Client.new.send(:serializer)
331 | iso_values.each {|k,v| serializer.decode_hash_value(k,v).must_be_kind_of Time, k}
332 | end
333 | end
334 |
335 | describe 'key does NOT match time_field pattern' do
336 | it 'ignores time-like values' do
337 | values = {
338 | :testat => '30 days',
339 | :teston => '2017-02-07T13:21:50-08:00',
340 | :testdate => '1968-04-03T12:23:34z',
341 | :range => '2 months ago for 1 month'
342 | }
343 |
344 | serializer = LookerSDK::Client.new.send(:serializer)
345 | values.each {|k,v| serializer.decode_hash_value(k,v).must_equal v, k}
346 | end
347 | end
348 | end
349 |
350 | # TODO: Convert the old tests that were here to deal with swagger/dynamic way of doing things. Perhaps
351 | # with a dedicated server that serves swagger customized to the test suite. Also, bring the auth tests
352 | # to life here on the SDK client end.
353 |
354 | end
355 |
--------------------------------------------------------------------------------
/lib/looker-sdk/client.rb:
--------------------------------------------------------------------------------
1 | ############################################################################################
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Looker Data Sciences, Inc.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ############################################################################################
24 |
25 | require 'sawyer'
26 | require 'ostruct'
27 | require 'looker-sdk/sawyer_patch'
28 | require 'looker-sdk/configurable'
29 | require 'looker-sdk/authentication'
30 | require 'looker-sdk/rate_limit'
31 | require 'looker-sdk/client/dynamic'
32 |
33 | module LookerSDK
34 |
35 | # Client for the LookerSDK API
36 | #
37 | # @see look TODO docs link
38 | class Client
39 |
40 | include LookerSDK::Authentication
41 | include LookerSDK::Configurable
42 | include LookerSDK::Client::Dynamic
43 |
44 | # Header keys that can be passed in options hash to {#get},{#head}
45 | CONVENIENCE_HEADERS = Set.new([:accept, :content_type])
46 |
47 | def initialize(opts = {})
48 | # Use options passed in, but fall back to module defaults
49 | LookerSDK::Configurable.keys.each do |key|
50 | instance_variable_set(:"@#{key}", opts[key] || LookerSDK.instance_variable_get(:"@#{key}"))
51 | end
52 |
53 | # allow caller to do configuration in a block before we load swagger and become dynamic
54 | yield self if block_given?
55 |
56 | # Save the original state of the options because live variables received later like access_token and
57 | # client_id appear as if they are options and confuse the automatic client generation in LookerSDK#client
58 | @original_options = options.dup
59 |
60 | load_credentials_from_netrc unless application_credentials?
61 | if !@lazy_swagger
62 | load_swagger
63 | end
64 | self.dynamic = true
65 | end
66 |
67 | # Compares client options to a Hash of requested options
68 | #
69 | # @param opts [Hash] Options to compare with current client options
70 | # @return [Boolean]
71 | def same_options?(opts)
72 | opts.hash == @original_options.hash
73 | end
74 |
75 | # Text representation of the client, masking tokens and passwords
76 | #
77 | # @return [String]
78 | def inspect
79 | inspected = super
80 |
81 | # Only show last 4 of token, secret
82 | [@access_token, @client_secret].compact.each do |str|
83 | len = [str.size - 4, 0].max
84 | inspected = inspected.gsub! str, "#{'*'*len}#{str[len..-1]}"
85 | end
86 |
87 | inspected
88 | end
89 |
90 | # Make a HTTP GET request
91 | #
92 | # @param url [String] The path, relative to {#api_endpoint}
93 | # @param options [Hash] Query and header params for request
94 | # @param encoded [Boolean] true: url already encoded, false: url needs encoding
95 | # @param &block [Block] Block to be called with |response, chunk| for each chunk of the body from
96 | # the server. The block must return true to continue, or false to abort streaming.
97 | # @return [Sawyer::Resource]
98 | def get(url, options = {}, encoded=false, &block)
99 | request :get, url, nil, parse_query_and_convenience_headers(options), encoded, &block
100 | end
101 |
102 | # Make a HTTP POST request
103 | #
104 | # @param url [String] The path, relative to {#api_endpoint}
105 | # @param data [String|Array|Hash] Body and optionally header params for request
106 | # @param options [Hash] Optional header params for request
107 | # @param encoded [Boolean] true: url already encoded, false: url needs encoding
108 | # @param &block [Block] Block to be called with |response, chunk| for each chunk of the body from
109 | # the server. The block must return true to continue, or false to abort streaming.
110 | # @return [Sawyer::Resource]
111 | def post(url, data = {}, options = {}, encoded=false, &block)
112 | request :post, url, data, parse_query_and_convenience_headers(options), encoded, &block
113 | end
114 |
115 | # Make a HTTP PUT request
116 | #
117 | # @param url [String] The path, relative to {#api_endpoint}
118 | # @param data [String|Array|Hash] Body and optionally header params for request
119 | # @param options [Hash] Optional header params for request
120 | # @param encoded [Boolean] true: url already encoded, false: url needs encoding
121 | # @param &block [Block] Block to be called with |response, chunk| for each chunk of the body from
122 | # the server. The block must return true to continue, or false to abort streaming.
123 | # @return [Sawyer::Resource]
124 | def put(url, data = {}, options = {}, encoded=false, &block)
125 | request :put, url, data, parse_query_and_convenience_headers(options), encoded, &block
126 | end
127 |
128 | # Make a HTTP PATCH request
129 | #
130 | # @param url [String] The path, relative to {#api_endpoint}
131 | # @param data [String|Array|Hash] Body and optionally header params for request
132 | # @param options [Hash] Optional header params for request
133 | # @param encoded [Boolean] true: url already encoded, false: url needs encoding
134 | # @param &block [Block] Block to be called with |response, chunk| for each chunk of the body from
135 | # the server. The block must return true to continue, or false to abort streaming.
136 | # @return [Sawyer::Resource]
137 | def patch(url, data = {}, options = {}, encoded=false, &block)
138 | request :patch, url, data, parse_query_and_convenience_headers(options), encoded, &block
139 | end
140 |
141 | # Make a HTTP DELETE request
142 | #
143 | # @param url [String] The path, relative to {#api_endpoint}
144 | # @param options [Hash] Query and header params for request
145 | # @param encoded [Boolean] true: url already encoded, false: url needs encoding
146 | # @return [Sawyer::Resource]
147 | def delete(url, options = {}, encoded=false, &block)
148 | request :delete, url, nil, parse_query_and_convenience_headers(options), encoded, &block
149 | end
150 |
151 | # Make a HTTP HEAD request
152 | #
153 | # @param url [String] The path, relative to {#api_endpoint}
154 | # @param options [Hash] Query and header params for request
155 | # @param encoded [Boolean] true: url already encoded, false: url needs encoding
156 | # @return [Sawyer::Resource]
157 | def head(url, options = {}, encoded=false, &block)
158 | request :head, url, nil, parse_query_and_convenience_headers(options), encoded
159 | end
160 |
161 | # Make one or more HTTP GET requests, optionally fetching
162 | # the next page of results from URL in Link response header based
163 | # on value in {#auto_paginate}.
164 | #
165 | # @param url [String] The path, relative to {#api_endpoint}
166 | # @param options [Hash] Query and header params for request
167 | # @param block [Block] Block to perform the data concatination of the
168 | # multiple requests. The block is called with two parameters, the first
169 | # contains the contents of the requests so far and the second parameter
170 | # contains the latest response.
171 | # @return [Sawyer::Resource]
172 | def paginate(url, options = {}, &block)
173 | opts = parse_query_and_convenience_headers(options)
174 | if @auto_paginate || @per_page
175 | opts[:query][:per_page] ||= @per_page || (@auto_paginate ? 100 : nil)
176 | end
177 |
178 | data = request(:get, url, nil, opts)
179 |
180 | if @auto_paginate
181 | while @last_response.rels[:next] && rate_limit.remaining > 0
182 | @last_response = @last_response.rels[:next].get
183 | if block_given?
184 | yield(data, @last_response)
185 | else
186 | data.concat(@last_response.data) if @last_response.data.is_a?(Array)
187 | end
188 | end
189 |
190 | end
191 |
192 | data
193 | end
194 |
195 | # Hypermedia agent for the LookerSDK API (with specific options)
196 | #
197 | # @return [Sawyer::Agent]
198 | def make_agent(options = nil)
199 | options ||= sawyer_options
200 | Sawyer::Agent.new(api_endpoint, options) do |http|
201 | http.headers[:accept] = default_media_type
202 | http.headers[:user_agent] = user_agent
203 | http.headers[:authorization] = "token #{@access_token}" if token_authenticated?
204 | end
205 | end
206 |
207 | # Cached Hypermedia agent for the LookerSDK API (with default options)
208 | #
209 | # @return [Sawyer::Agent]
210 | def agent
211 | @agent ||= make_agent
212 | end
213 |
214 | # Fetch the root resource for the API
215 | #
216 | # @return [Sawyer::Resource]
217 | def root
218 | get URI(api_endpoint).path.sub(/\/$/,'')
219 | end
220 |
221 | # Is the server alive (this can be called w/o authentication)
222 | #
223 | # @return http status code
224 | def alive
225 | without_authentication do
226 | get '/alive'
227 | end
228 | last_response.status
229 | end
230 |
231 | # Are we connected to the server? - Does not attempt to authenticate.
232 | def alive?
233 | begin
234 | without_authentication do
235 | get('/alive')
236 | end
237 | true
238 | rescue
239 | false
240 | end
241 | end
242 |
243 | # Are we connected and authenticated to the server?
244 | def authenticated?
245 | begin
246 | ensure_logged_in
247 | true
248 | rescue
249 | false
250 | end
251 | end
252 |
253 | # Response for last HTTP request
254 | #
255 | # @return [Sawyer::Response]
256 | def last_response
257 | @last_response if defined? @last_response
258 | end
259 |
260 | # Response for last HTTP request
261 | #
262 | # @return [StandardError]
263 | def last_error
264 | @last_error if defined? @last_error
265 | end
266 |
267 | # Set OAuth access token for authentication
268 | #
269 | # @param value [String] Looker OAuth access token
270 | def access_token=(value)
271 | reset_agent
272 | @access_token = value
273 | end
274 |
275 | # Set OAuth app client_id
276 | #
277 | # @param value [String] Looker OAuth app client_id
278 | def client_id=(value)
279 | reset_agent
280 | @client_id = value
281 | end
282 |
283 | # Set OAuth app client_secret
284 | #
285 | # @param value [String] Looker OAuth app client_secret
286 | def client_secret=(value)
287 | reset_agent
288 | @client_secret = value
289 | end
290 |
291 | # Wrapper around Kernel#warn to print warnings unless
292 | # LOOKER_SILENT is set to true.
293 | #
294 | # @return [nil]
295 | def looker_warn(*message)
296 | unless ENV['LOOKER_SILENT']
297 | warn message
298 | end
299 | end
300 |
301 | private
302 |
303 | def reset_agent
304 | @agent = nil
305 | end
306 |
307 | def request(method, path, data, options, encoded, &block)
308 | ensure_logged_in
309 | begin
310 | path = path.to_s
311 | if !encoded
312 | path = URI::Parser.new.escape(path)
313 | end
314 | @last_response = @last_error = nil
315 | return stream_request(method, path, data, options, &block) if block_given?
316 | @last_response = response = agent.call(method, path, data, options)
317 | @raw_responses ? response : response.data
318 | rescue StandardError => e
319 | @last_error = e
320 | raise
321 | end
322 | end
323 |
324 | def stream_request(method, path, data, options, &block)
325 | conn_opts = faraday_options(:builder => StreamingClient.new(self, &block))
326 | agent = make_agent(sawyer_options(:faraday => Faraday.new(conn_opts)))
327 | @last_response = agent.call(method, URI::Parser.new.escape(path.to_s), data, options)
328 | end
329 |
330 | # Since Faraday currently won't do streaming for us, we use Net::HTTP. Still, we go to the trouble
331 | # to go through the Sawyer/Faraday codepath so that we can leverage all the header and param
332 | # processing they do in order to be as consistent as we can with the normal non-streaming codepath.
333 | # This class replaces the default Faraday 'builder' that Faraday uses to do the actual request after
334 | # all the setup is done.
335 |
336 | class StreamingClient
337 | class Progress
338 | attr_reader :response
339 | attr_accessor :chunks, :length
340 |
341 | def initialize(response)
342 | @response = response
343 | @chunks = @length = 0
344 | @stopped = false
345 | end
346 |
347 | def add_chunk(chunk)
348 | @chunks += 1
349 | @length += chunk.length
350 | end
351 |
352 | def stop
353 | @stopped = true
354 | end
355 |
356 | def stopped?
357 | @stopped
358 | end
359 | end
360 |
361 | def initialize(client, &block)
362 | @client, @block = client, block
363 | end
364 |
365 | # This is the method that faraday calls on a builder to do the actual request and build a response.
366 | def build_response(connection, request)
367 | full_path = connection.build_exclusive_url(request.path, request.params,
368 | request.options.params_encoder).to_s
369 | uri = URI(full_path)
370 | path_with_query = uri.query ? "#{uri.path}?#{uri.query}" : uri.path
371 |
372 | http_request = (
373 | case request.method
374 | when :get then Net::HTTP::Get
375 | when :post then Net::HTTP::Post
376 | when :put then Net::HTTP::Put
377 | when :patch then Net::HTTP::Patch
378 | else raise "Stream to block not supported for '#{request.method}'"
379 | end
380 | ).new(path_with_query, request.headers)
381 |
382 | http_request.body = request.body
383 |
384 | connect_opts = {
385 | :use_ssl => !!connection.ssl,
386 | :verify_mode => (connection.ssl.verify rescue true) ?
387 | OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE,
388 | }
389 |
390 | # TODO: figure out how/if to support proxies
391 | # TODO: figure out how to test this comprehensively
392 |
393 | progress = nil
394 | Net::HTTP.start(uri.host, uri.port, connect_opts) do |http|
395 | http.open_timeout = connection.options.open_timeout rescue 30
396 | http.read_timeout = connection.options.timeout rescue 60
397 |
398 | http.request(http_request) do |response|
399 | progress = Progress.new(response)
400 | if response.code == "200"
401 | response.read_body do |chunk|
402 | next unless chunk.length > 0
403 | progress.add_chunk(chunk)
404 | @block.call(chunk, progress)
405 | return OpenStruct.new(status:"0", headers:{}, env:nil, body:nil) if progress.stopped?
406 | end
407 | end
408 | end
409 | end
410 |
411 | return OpenStruct.new(status:"500", headers:{}, env:nil, body:nil) unless progress
412 |
413 | OpenStruct.new(status:progress.response.code, headers:progress.response, env:nil, body:nil)
414 | end
415 | end
416 |
417 | def delete_succeeded?
418 | !!last_response && last_response.status == 204
419 | end
420 |
421 | class Serializer < Sawyer::Serializer
422 | def encode(data)
423 | data.kind_of?(Faraday::UploadIO) ? data : super
424 | end
425 |
426 | # slight modification to the base class' decode_hash_value function to
427 | # less permissive when decoding time values.
428 | # also prevent conversion from non-string types to Time e.g. integer/float timestamp
429 | #
430 | # See https://github.com/looker/looker-sdk-ruby/issues/53 for more details
431 | #
432 | # Base class function that we're overriding: https://github.com/lostisland/sawyer/blob/master/lib/sawyer/serializer.rb#L101-L121
433 | def decode_hash_value(key, value)
434 | if time_field?(key, value)
435 | if value.is_a?(String)
436 | begin
437 | Time.iso8601(value)
438 | rescue ArgumentError
439 | value
440 | end
441 | else
442 | value
443 | end
444 | else
445 | super
446 | end
447 | end
448 | end
449 |
450 | def serializer
451 | @serializer ||= (
452 | require 'json'
453 | Serializer.new(JSON)
454 | )
455 | end
456 |
457 | def faraday_options(options = {})
458 | conn_opts = @connection_options.clone
459 | builder = options[:builder] || @middleware
460 | conn_opts[:builder] = builder if builder
461 | conn_opts[:proxy] = @proxy if @proxy
462 | conn_opts
463 | end
464 |
465 | def sawyer_options(options = {})
466 | {
467 | :links_parser => Sawyer::LinkParsers::Simple.new,
468 | :serializer => serializer,
469 | :faraday => options[:faraday] || @faraday || Faraday.new(faraday_options)
470 | }
471 | end
472 |
473 | def merge_content_type_if_body(body, options = {})
474 | if body
475 | if body.kind_of?(Faraday::UploadIO)
476 | length = File.new(body.local_path).size.to_s
477 | headers = {:content_type => body.content_type, :content_length => length}.merge(options[:headers] || {})
478 | else
479 | headers = {:content_type => default_media_type}.merge(options[:headers] || {})
480 | end
481 | {:headers => headers}.merge(options)
482 | else
483 | options
484 | end
485 | end
486 |
487 | def parse_query_and_convenience_headers(options)
488 | return {} if options.nil?
489 | raise "options is not a hash" unless options.is_a?(Hash)
490 | return {} if options.empty?
491 |
492 | options = options.dup
493 | headers = options.delete(:headers) || {}
494 | CONVENIENCE_HEADERS.each do |h|
495 | if header = options.delete(h)
496 | headers[h] = header
497 | end
498 | end
499 | query = options.delete(:query) || {}
500 | raise "query '#{query}' is not a hash" unless query.is_a?(Hash)
501 | query = options.merge(query)
502 |
503 | opts = {}
504 | opts[:query] = query unless query.empty?
505 | opts[:headers] = headers unless headers.empty?
506 |
507 | opts
508 | end
509 | end
510 | end
511 |
--------------------------------------------------------------------------------