├── .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 [![Build Status](https://travis-ci.org/looker/looker-sdk-ruby.svg)](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 | --------------------------------------------------------------------------------