├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── csharp └── README.md ├── java └── README.md ├── kotlin └── README.md ├── looker-sample.ini ├── python ├── .flake8 ├── .python-version ├── Pipfile ├── Pipfile.lock ├── README.md ├── __init__.py ├── content_validator_comparison.py ├── create_db_connections.py ├── disable_users_by_email.py ├── download_dashboard_pdf.py ├── download_look.py ├── download_tile.py ├── hackathon_app │ ├── .flake8 │ ├── .gitignore │ ├── Dockerfile │ ├── Pipfile │ ├── Pipfile.lock │ ├── README.md │ ├── authentication.py │ ├── build-docker.sh │ ├── bulk_import.py │ ├── config.py │ ├── create-test-sheet.py │ ├── create-test-sheet.sh │ ├── env.list.sample │ ├── frontend │ │ ├── .gitignore │ │ ├── .nvmrc │ │ ├── .prettierrc.json │ │ ├── README.md │ │ ├── package.json │ │ ├── public │ │ │ ├── favicon.ico │ │ │ ├── index.html │ │ │ ├── logo192.png │ │ │ ├── logo512.png │ │ │ ├── manifest.json │ │ │ └── robots.txt │ │ ├── src │ │ │ ├── App.css │ │ │ ├── App.test.tsx │ │ │ ├── App.tsx │ │ │ ├── Logo.tsx │ │ │ ├── Looker_Logo_Purple.svg │ │ │ ├── RegisterScene.tsx │ │ │ ├── ResourcesScene.tsx │ │ │ ├── index.css │ │ │ ├── index.tsx │ │ │ ├── logo.svg │ │ │ ├── react-app-env.d.ts │ │ │ └── serviceWorker.ts │ │ ├── tsconfig.json │ │ └── yarn.lock │ ├── hackitecture.jpg │ ├── logging.conf │ ├── looker.py │ ├── mypy.ini │ ├── run-docker.sh │ ├── run-tests.sh │ ├── server │ │ ├── __init__.py │ │ └── main.py │ ├── sheets.py │ ├── start-dev-flask.sh │ ├── stop-docker.sh │ └── tests │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── data │ │ └── data.json │ │ ├── integration │ │ ├── __init__.py │ │ ├── test_hackathons.py │ │ ├── test_looker.py │ │ ├── test_registrations.py │ │ ├── test_sheets.py │ │ └── test_users.py │ │ └── unit │ │ ├── test_authentication.py │ │ └── test_wholly_sheet.py ├── logout_all_users.py ├── lookml_model_explore.py ├── mypy.ini ├── oauth_session.py ├── run_look_with_filters.py ├── sdk_exceptions.py ├── soft_delete_dashboard.py └── test_connection.py ├── r └── README.md ├── ruby ├── README.md ├── all_git_branches.rb ├── all_users_to_group.rb ├── create_themes.rb ├── delete_unused_content.rb ├── dev_vs_prod.rb ├── disable_users.rb ├── download_dashboard.rb ├── kill_all_running_queries.rb ├── list_files_per_project.rb ├── list_users_and_auth_type.rb ├── logout_all_users.rb ├── render_look_png.rb ├── rerun_failed_email_schedules.rb ├── schedule_once_to_gcs.rb ├── stream_to_s3.rb ├── test_all_connections.rb ├── test_integrations.rb ├── update_look.rb ├── users_list_and_auth_types.rb ├── validate_content.rb └── validate_projects.rb ├── swift ├── README.md └── sample-swift-sdk │ ├── README.md │ ├── sample-swift-sdk.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ └── Macallan.xcuserdatad │ │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ │ └── xcschemes │ │ └── xcschememanagement.plist │ ├── sample-swift-sdk │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── Dashboards.swift │ ├── Info.plist │ ├── LookerSwiftSDK │ │ ├── apiConfig.swift │ │ ├── apiMethods.swift │ │ ├── apiSettings.swift │ │ ├── authSession.swift │ │ ├── authToken.swift │ │ ├── baseTransport.swift │ │ ├── constants.swift │ │ ├── looker.swift │ │ ├── methods.swift │ │ ├── models.swift │ │ ├── partial.swift │ │ ├── serializer.swift │ │ └── transport.swift │ ├── MainView.swift │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ └── SceneDelegate.swift │ ├── sample-swift-sdkTests │ ├── Info.plist │ └── sample_swift_sdkTests.swift │ └── sample-swift-sdkUITests │ ├── Info.plist │ └── sample_swift_sdkUITests.swift └── typescript ├── README.md ├── customConfigReader.ts ├── downloadDashboard.ts ├── downloadTile.ts ├── dual.ts ├── index.ts ├── package.json ├── sudoAsUser.ts ├── tsconfig.json ├── tslint.json ├── utils.ts └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | **/.idea 2 | **/looker.ini 3 | **/node_modules 4 | **/package-lock.json 5 | **/.mypy_cache/ 6 | **/.vscode 7 | **/.__pycache__ 8 | **/*.xcscheme 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at opensource@looker.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This repository is archived 2 | 3 | The [SDK codegen project examples directory](https://github.com/looker-open-source/sdk-codegen/tree/master/examples) is the new home of Looker's SDK examples. 4 | 5 | # Examples for the Looker SDKs 6 | 7 | This repository contains source code examples and projects that use SDKs to communicate with the Looker API. Examples are organized by folder names that correspond to the name of the programming language used. 8 | 9 | This is a constantly-evolving repository, where examples could change, appear, and disappear at any time. 10 | 11 | ## Please contribute 12 | 13 | All are welcome to submit examples. Please feel free to submit a PR for any examples you may want to share. Thank you! 14 | -------------------------------------------------------------------------------- /csharp/README.md: -------------------------------------------------------------------------------- 1 | # C# Examples for the Looker API 2 | 3 | There are currently no C# examples for the Looker API. Feel free to submit some! 4 | -------------------------------------------------------------------------------- /java/README.md: -------------------------------------------------------------------------------- 1 | # Java Examples for the Looker API 2 | 3 | You can find Java language examples in this folder. 4 | -------------------------------------------------------------------------------- /kotlin/README.md: -------------------------------------------------------------------------------- 1 | # Kotlin Examples for the Looker API 2 | 3 | You can find Kotlin language examples in this folder. 4 | -------------------------------------------------------------------------------- /looker-sample.ini: -------------------------------------------------------------------------------- 1 | [Looker] 2 | # API version is required. 3.1 and 3.0 are currently supported. 3.1 is the current version. 3.0 is deprecated. 3 | api_version=3.1 4 | # Base URL for API. Do not include /api/* in the url 5 | base_url=https://:19999 6 | # API 3 client id 7 | client_id=your_API3_client_id 8 | # API 3 client secret 9 | client_secret=your_API3_client_secret 10 | # Set to false only if testing locally against self-signed certs. Defaults to true if not specified 11 | verify_ssl=true 12 | # Timeout in seconds for requests. Defaults to 2 minutes (120) seconds if not specified. 13 | timeout=120 14 | -------------------------------------------------------------------------------- /python/.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E203, E266, E501, W503 3 | max-line-length = 80 4 | max-complexity = 18 5 | select = B,C,E,F,W,T4,B9 6 | -------------------------------------------------------------------------------- /python/.python-version: -------------------------------------------------------------------------------- 1 | 3.8.1 2 | -------------------------------------------------------------------------------- /python/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | looker-sdk = "*" 10 | mypy = "*" 11 | black = "*" 12 | flake8 = "*" 13 | flake8-bugbear = "*" 14 | 15 | [requires] 16 | python_version = "3.8" 17 | 18 | [pipenv] 19 | allow_prereleases = true 20 | -------------------------------------------------------------------------------- /python/README.md: -------------------------------------------------------------------------------- 1 | # Python Examples for the Looker API 2 | 3 | You can find Python language examples in this folder. 4 | 5 | ## Connection management 6 | 7 | - [Test a specified connection](test_connection.py) 8 | 9 | ## Manage Dashboards 10 | 11 | - [Soft delete dashboard](soft_delete_dashboard.py) 12 | 13 | ## Manage Render Tasks 14 | 15 | - [Download dashboard tile in specified format](download_tile.py) 16 | - [Download look in specified format](download_look.py) 17 | - [Generate and download dashboard PDFs](download_dashboard_pdf.py) 18 | 19 | ## User Management 20 | 21 | - [Disable all active user sessions](logout_all_users.py) 22 | -------------------------------------------------------------------------------- /python/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/looker-open-source/sdk-examples/a7eff0c2cd9724e8e548aba102b047e10a8dcbc3/python/__init__.py -------------------------------------------------------------------------------- /python/content_validator_comparison.py: -------------------------------------------------------------------------------- 1 | import looker_sdk 2 | from looker_sdk import models 3 | from looker_sdk.rtl import transport 4 | import configparser 5 | import hashlib 6 | import csv 7 | 8 | config_file = "../looker.ini" 9 | sdk = looker_sdk.init31() 10 | 11 | 12 | def main(): 13 | """Compare the output of content validator runs 14 | in production and development mode. Additional 15 | broken content in development mode will be 16 | outputted to a csv file. 17 | 18 | Use this script to test whether LookML changes 19 | will result in new broken content.""" 20 | base_url = get_base_url() 21 | space_data = get_space_data() 22 | print("Checking for broken content in production.") 23 | broken_content_prod = parse_broken_content( 24 | base_url, get_broken_content(), space_data 25 | ) 26 | checkout_dev_branch() 27 | print("Checking for broken content in dev branch.") 28 | broken_content_dev = parse_broken_content( 29 | base_url, get_broken_content(), space_data 30 | ) 31 | new_broken_content = compare_broken_content(broken_content_prod, broken_content_dev) 32 | if new_broken_content: 33 | write_broken_content_to_file(new_broken_content, "new_broken_content.csv") 34 | else: 35 | print("No new broken content in development branch.") 36 | 37 | 38 | def get_base_url(): 39 | """ Pull base url from looker.ini, remove port""" 40 | config = configparser.ConfigParser() 41 | config.read(config_file) 42 | full_base_url = config.get("Looker", "base_url") 43 | base_url = sdk.auth.settings.base_url[: full_base_url.index(":19999")] 44 | return base_url 45 | 46 | 47 | def get_space_data(): 48 | """Collect all space information""" 49 | space_data = sdk.all_spaces(fields="id, parent_id, name") 50 | return space_data 51 | 52 | 53 | def get_broken_content(): 54 | """Collect broken content""" 55 | broken_content = sdk.content_validation( 56 | transport_options=transport.TransportSettings(timeout=600) 57 | ).content_with_errors 58 | return broken_content 59 | 60 | 61 | def parse_broken_content(base_url, broken_content, space_data): 62 | """Parse and return relevant data from content validator""" 63 | output = [] 64 | for item in broken_content: 65 | if item.dashboard: 66 | content_type = "dashboard" 67 | else: 68 | content_type = "look" 69 | item_content_type = getattr(item, content_type) 70 | id = item_content_type.id 71 | name = item_content_type.title 72 | space_id = item_content_type.space.id 73 | space_name = item_content_type.space.name 74 | errors = item.errors 75 | url = f"{base_url}/{content_type}s/{id}" 76 | space_url = "{}/spaces/{}".format(base_url, space_id) 77 | if content_type == "look": 78 | element = None 79 | else: 80 | dashboard_element = item.dashboard_element 81 | element = dashboard_element.title if dashboard_element else None 82 | # Lookup additional space information 83 | space = next(i for i in space_data if str(i.id) == str(space_id)) 84 | parent_space_id = space.parent_id 85 | # Old version of API has issue with None type for all_space() call 86 | if parent_space_id is None or parent_space_id == "None": 87 | parent_space_url = None 88 | parent_space_name = None 89 | else: 90 | parent_space_url = "{}/spaces/{}".format(base_url, parent_space_id) 91 | parent_space = next( 92 | (i for i in space_data if str(i.id) == str(parent_space_id)), None 93 | ) 94 | # Handling an edge case where space has no name. This can happen 95 | # when users are improperly generated with the API 96 | try: 97 | parent_space_name = parent_space.name 98 | except AttributeError: 99 | parent_space_name = None 100 | # Create a unique hash for each record. This is used to compare 101 | # results across content validator runs 102 | unique_id = hashlib.md5( 103 | "-".join( 104 | [str(id), str(element), str(name), str(errors), str(space_id)] 105 | ).encode() 106 | ).hexdigest() 107 | data = { 108 | "unique_id": unique_id, 109 | "content_type": content_type, 110 | "name": name, 111 | "url": url, 112 | "dashboard_element": element, 113 | "space_name": space_name, 114 | "space_url": space_url, 115 | "parent_space_name": parent_space_name, 116 | "parent_space_url": parent_space_url, 117 | "errors": str(errors), 118 | } 119 | output.append(data) 120 | return output 121 | 122 | 123 | def compare_broken_content(broken_content_prod, broken_content_dev): 124 | """Compare output between 2 content_validation runs""" 125 | unique_ids_prod = set([i["unique_id"] for i in broken_content_prod]) 126 | unique_ids_dev = set([i["unique_id"] for i in broken_content_dev]) 127 | new_broken_content_ids = unique_ids_dev.difference(unique_ids_prod) 128 | new_broken_content = [] 129 | for item in broken_content_dev: 130 | if item["unique_id"] in new_broken_content_ids: 131 | new_broken_content.append(item) 132 | return new_broken_content 133 | 134 | 135 | def checkout_dev_branch(): 136 | """Enter dev workspace""" 137 | sdk.update_session(models.WriteApiSession(workspace_id="dev")) 138 | 139 | 140 | def write_broken_content_to_file(broken_content, output_csv_name): 141 | """Export new content errors in dev branch to csv file""" 142 | try: 143 | with open(output_csv_name, "w") as csvfile: 144 | writer = csv.DictWriter(csvfile, fieldnames=list(broken_content[0].keys())) 145 | writer.writeheader() 146 | for data in broken_content: 147 | writer.writerow(data) 148 | print("Broken content information outputed to {}".format(output_csv_name)) 149 | except IOError: 150 | print("I/O error") 151 | 152 | 153 | main() 154 | -------------------------------------------------------------------------------- /python/create_db_connections.py: -------------------------------------------------------------------------------- 1 | import looker_sdk 2 | from looker_sdk import models 3 | import base64 4 | 5 | with open("credentials_file.json", "rb") as f: 6 | cert = f.read() 7 | 8 | 9 | cert = base64.b64encode(cert).decode("utf-8") 10 | """Base64 encoded Certificate body for server authentication""" 11 | 12 | 13 | sdk = looker_sdk.init31("looker.ini") 14 | 15 | 16 | sdk.create_connection( 17 | models.WriteDBConnection( 18 | name="api created", 19 | host="host_name", 20 | database="your_db", 21 | dialect_name="bigquery_standard_sql", 22 | certificate=cert, 23 | username="user_name", 24 | file_type=".json", 25 | ) 26 | ) 27 | -------------------------------------------------------------------------------- /python/disable_users_by_email.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import csv 3 | 4 | import looker_sdk 5 | 6 | """ 7 | The purpose of this script is to parse a CSV file containing a list of 8 | emails separated by line breaks. If the email in the file corresponds 9 | to an email in the Looker instance referred to in looker.ini, that user will 10 | be automatically disabled. 11 | """ 12 | 13 | 14 | sdk = looker_sdk.init31("../looker.ini") 15 | 16 | 17 | def disable_user(user): 18 | print("Disabling user " + user.email + " with id " + str(user.id)) 19 | user.is_disabled = True 20 | sdk.update_user(user_id=user.id, body=user) 21 | 22 | 23 | def parse_csv(): 24 | f = open( 25 | args.filename, 26 | "r", 27 | # the encoding may need to be updated depending on your file, 28 | # see https://stackoverflow.com/a/17912811 29 | encoding="utf-8-sig", 30 | ) 31 | csv_reader = csv.reader(f, delimiter=",") 32 | 33 | for line in csv_reader: 34 | for user in sdk.search_users(fields="id,email", email=line[0]): 35 | disable_user(user) 36 | 37 | 38 | if __name__ == "__main__": 39 | parser = argparse.ArgumentParser() 40 | parser.add_argument( 41 | "--filename", 42 | help="The path to the CSV file containing a list of emails to disable.", 43 | required=True, 44 | ) 45 | args = parser.parse_args() 46 | parse_csv() 47 | -------------------------------------------------------------------------------- /python/download_dashboard_pdf.py: -------------------------------------------------------------------------------- 1 | import json 2 | import urllib 3 | import sys 4 | import textwrap 5 | import time 6 | from typing import cast, Dict, Optional 7 | 8 | import looker_sdk 9 | from looker_sdk import models 10 | 11 | import sdk_exceptions 12 | 13 | sdk = looker_sdk.init31("../looker.ini") 14 | 15 | 16 | def main(): 17 | """Given a dashboard title, search all dashboards to retrieve its id and use 18 | it to render the dashboard's pdf. 19 | 20 | Examples of how to use this: 21 | $ python download_dashboard_pdf.py "A Test Dashboard" 22 | $ python download_dashboard_pdf.py "A Test Dashboard" '{"filter1": "value1, value2", "filter2": "value3"}' 23 | $ python download_dashboard_pdf.py "A Test Dashboard" {} "single_column" 24 | """ 25 | dashboard_title = sys.argv[1] if len(sys.argv) > 1 else "" 26 | filters = json.loads(sys.argv[2]) if len(sys.argv) > 2 else None 27 | pdf_style = sys.argv[3] if len(sys.argv) > 3 else "tiled" 28 | pdf_width = int(sys.argv[4]) if len(sys.argv) > 4 else 545 29 | pdf_height = int(sys.argv[5]) if len(sys.argv) > 5 else 842 30 | 31 | if not dashboard_title: 32 | raise sdk_exceptions.ArgumentError( 33 | textwrap.dedent( 34 | """ 35 | Please provide: [] [] [] [] 36 | dashboard_style defaults to "tiled" 37 | pdf_width defaults to 545 38 | pdf_height defaults to 842""" 39 | ) 40 | ) 41 | 42 | dashboard = cast(models.Dashboard, get_dashboard(dashboard_title)) 43 | download_dashboard(dashboard, pdf_style, pdf_width, pdf_height, filters) 44 | 45 | 46 | def get_dashboard(title: str) -> Optional[models.Dashboard]: 47 | """Get a dashboard by title.""" 48 | title = title.lower() 49 | dashboard = next(iter(sdk.search_dashboards(title=title)), None) 50 | if not dashboard: 51 | raise sdk_exceptions.NotFoundError(f'dashboard "{title}" not found') 52 | assert isinstance(dashboard, models.Dashboard) 53 | return dashboard 54 | 55 | 56 | def download_dashboard( 57 | dashboard: models.Dashboard, 58 | style: str = "tiled", 59 | width: int = 545, 60 | height: int = 842, 61 | filters: Optional[Dict[str, str]] = None, 62 | ): 63 | """Download specified dashboard as PDF""" 64 | assert dashboard.id 65 | id = int(dashboard.id) 66 | task = sdk.create_dashboard_render_task( 67 | id, 68 | "pdf", 69 | models.CreateDashboardRenderTask( 70 | dashboard_style=style, 71 | dashboard_filters=urllib.parse.urlencode(filters) if filters else None, 72 | ), 73 | width, 74 | height, 75 | ) 76 | 77 | if not (task and task.id): 78 | raise sdk_exceptions.RenderTaskError( 79 | f'Could not create a render task for "{dashboard.title}"' 80 | ) 81 | 82 | # poll the render task until it completes 83 | elapsed = 0.0 84 | delay = 0.5 # wait .5 seconds 85 | while True: 86 | poll = sdk.render_task(task.id) 87 | if poll.status == "failure": 88 | print(poll) 89 | raise sdk_exceptions.RenderTaskError( 90 | f'Render failed for "{dashboard.title}"' 91 | ) 92 | elif poll.status == "success": 93 | break 94 | 95 | time.sleep(delay) 96 | elapsed += delay 97 | print(f"Render task completed in {elapsed} seconds") 98 | 99 | result = sdk.render_task_results(task.id) 100 | filename = f"{dashboard.title}.pdf" 101 | with open(filename, "wb") as f: 102 | f.write(result) 103 | print(f'Dashboard pdf saved to "{filename}"') 104 | 105 | 106 | main() 107 | -------------------------------------------------------------------------------- /python/download_look.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import textwrap 3 | import time 4 | 5 | import looker_sdk 6 | from looker_sdk import models 7 | 8 | import sdk_exceptions 9 | 10 | sdk = looker_sdk.init31("../looker.ini") 11 | 12 | 13 | def main(): 14 | """Given a look title, find the corresponding look id and use 15 | it to render its image. 16 | 17 | $ python download_look.py "A good look" 1024 768 png 18 | """ 19 | look_title = sys.argv[1] if len(sys.argv) > 1 else "" 20 | image_width = int(sys.argv[2]) if len(sys.argv) > 2 else 545 21 | image_height = int(sys.argv[3]) if len(sys.argv) > 3 else 842 22 | image_format = sys.argv[4] if len(sys.argv) > 4 else "png" 23 | 24 | if not look_title: 25 | raise sdk_exceptions.ArgumentError( 26 | textwrap.dedent( 27 | """ 28 | Please provide: [] [] [] 29 | img_width defaults to 545 30 | img_height defaults to 842 31 | img_format defaults to 'png'""" 32 | ) 33 | ) 34 | 35 | look = get_look(look_title) 36 | download_look(look, image_format, image_width, image_height) 37 | 38 | 39 | def get_look(title: str) -> models.Look: 40 | title = title.lower() 41 | look = next(iter(sdk.search_looks(title=title)), None) 42 | if not look: 43 | raise sdk_exceptions.NotFoundError(f"look '{title}' was not found") 44 | assert isinstance(look, models.Look) 45 | return look 46 | 47 | 48 | def download_look(look: models.Look, result_format: str, width: int, height: int): 49 | """Download specified look as png/jpg""" 50 | assert look.id 51 | id = int(look.id) 52 | task = sdk.create_look_render_task(id, result_format, width, height,) 53 | 54 | if not (task and task.id): 55 | raise sdk_exceptions.RenderTaskError( 56 | f"Could not create a render task for '{look.title}'" 57 | ) 58 | 59 | # poll the render task until it completes 60 | elapsed = 0.0 61 | delay = 0.5 # wait .5 seconds 62 | while True: 63 | poll = sdk.render_task(task.id) 64 | if poll.status == "failure": 65 | print(poll) 66 | raise sdk_exceptions.RenderTaskError(f"Render failed for '{look.title}'") 67 | elif poll.status == "success": 68 | break 69 | time.sleep(delay) 70 | elapsed += delay 71 | print(f"Render task completed in {elapsed} seconds") 72 | 73 | result = sdk.render_task_results(task.id) 74 | filename = f"{look.title}.{result_format}" 75 | with open(filename, "wb") as f: 76 | f.write(result) 77 | print(f"Look saved to '{filename}'") 78 | 79 | 80 | main() 81 | -------------------------------------------------------------------------------- /python/download_tile.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | 4 | import looker_sdk 5 | from looker_sdk import models 6 | from looker_sdk.rtl import transport 7 | 8 | 9 | sdk = looker_sdk.init31("../looker.ini") 10 | 11 | 12 | def get_dashboard(title: str): 13 | """Get a dashboard by title""" 14 | dashboards = sdk.search_dashboards(title=title) 15 | if not dashboards: 16 | print(f"dashboard {title} was not found") 17 | return None 18 | return dashboards[0] 19 | 20 | 21 | def get_tile(dash: models.Dashboard, title: str): 22 | """Find a dashboard tile by title""" 23 | title = title.lower() 24 | assert dash.dashboard_elements 25 | found = None 26 | for tile in dash.dashboard_elements: 27 | assert tile.title 28 | if tile.title.lower() == title: 29 | found = tile 30 | break 31 | else: 32 | print(f"tile {title} of dashboard {dash.title} was not found") 33 | return found 34 | 35 | 36 | def download_tile(tile: models.DashboardElement, format: str = "png"): 37 | """Download the dashboard tile in the specified format""" 38 | if not tile.query_id: 39 | print(f"tile {tile.title} has no query_id") 40 | return None 41 | task = sdk.create_query_render_task( 42 | query_id=tile.query_id, result_format=format, width=500, height=500 43 | ) 44 | 45 | if not task or not task.id: 46 | print(f"Could not create a render task for {tile.title}") 47 | return None 48 | 49 | # poll the render task until it completes 50 | elapsed = 0.0 51 | delay = 0.5 # wait .5 seconds 52 | while True: 53 | poll = sdk.render_task(task.id) 54 | if poll.status == "failure": 55 | print(poll) 56 | print(f"Render failed for {tile.title}") 57 | return None 58 | elif poll.status == "success": 59 | break 60 | 61 | time.sleep(delay) 62 | elapsed += delay 63 | print(f"{elapsed} seconds elapsed") 64 | 65 | result = sdk.render_task_results( 66 | task.id, 67 | # wait up to 300 seconds 68 | transport_options=transport.TransportSettings(timeout=300), 69 | ) 70 | fileName = f"{tile.title}.{format}" 71 | with open(fileName, "wb") as f: 72 | f.write(result) 73 | return fileName 74 | 75 | 76 | def main(): 77 | dashboard_title = sys.argv[1] if len(sys.argv) > 1 else "" 78 | tile_title = sys.argv[2] if len(sys.argv) > 2 else "" 79 | render_format = sys.argv[3] if len(sys.argv) > 3 else "png" 80 | if not dashboard_title or not tile_title: 81 | print("Please provide: []") 82 | print(' renderFormat defaults to "png"') 83 | return 84 | 85 | dashboard = get_dashboard(dashboard_title) 86 | if dashboard: 87 | tile = get_tile(dashboard, tile_title) 88 | if tile: 89 | download_tile(tile, render_format) 90 | 91 | 92 | main() 93 | -------------------------------------------------------------------------------- /python/hackathon_app/.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E203, E266, E501, W503 3 | max-line-length = 80 4 | max-complexity = 18 5 | select = B,C,E,F,W,T4,B9 6 | -------------------------------------------------------------------------------- /python/hackathon_app/.gitignore: -------------------------------------------------------------------------------- 1 | env.list 2 | google-creds.json 3 | frontend/status.json 4 | -------------------------------------------------------------------------------- /python/hackathon_app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM tiangolo/uwsgi-nginx-flask:python3.7 2 | 3 | RUN pip install pipenv 4 | 5 | COPY ./server /app 6 | COPY Pipfile Pipfile.lock looker.py sheets.py config.py logging.conf authentication.py /app/ 7 | COPY env.list /app/prestart.sh 8 | 9 | WORKDIR /app 10 | 11 | RUN pipenv install --system 12 | 13 | ENV STATIC_INDEX 1 14 | 15 | COPY ./frontend/build /app/static 16 | -------------------------------------------------------------------------------- /python/hackathon_app/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | flake8 = "*" 8 | black = "*" 9 | mypy = "*" 10 | flask = "*" 11 | pudb = "*" 12 | ptipython = "*" 13 | pytest = "*" 14 | 15 | [packages] 16 | looker-sdk = "*" 17 | flask-wtf = "*" 18 | google-api-python-client = "*" 19 | google-auth = "*" 20 | python-json-logger = "*" 21 | click = "*" 22 | sendgrid = "*" 23 | cryptography = "*" 24 | typing-extensions = "*" 25 | 26 | [requires] 27 | python_version = "3.7" 28 | 29 | [pipenv] 30 | allow_prereleases = true 31 | -------------------------------------------------------------------------------- /python/hackathon_app/README.md: -------------------------------------------------------------------------------- 1 | # Hackathon App 2 | 3 | Welcome to the hackathon app: a web app using a React frontend and a python flask backend 4 | 5 | ## Getting setup for local development 6 | 7 | ### Frontend 8 | 9 | Node 12 is required for the frontend. You can use [nvm](https://github.com/creationix/nvm#installation) and [avn](https://github.com/wbyoung/avn#install) to easily switch Node versions between different projects. 10 | 11 | From `sdk-examples/python/hackathon_app/` directory: 12 | ```sh 13 | cd frontend/ 14 | yarn install 15 | yarn start 16 | ``` 17 | 18 | You can work on the frontend React app without running the python server or having any backend google sheets or looker instance dependencies. Simply skip the remaining setup. 19 | 20 | ### Backend 21 | 22 | hackathon_app requires python 3.7+ The easiest way to get this installed on a mac is to install [pyenv](https://github.com/pyenv/pyenv#installation) via [homebrew](https://brew.sh/). If you already have pyenv and pipenv installed then skip to the "Install all dependencies" step 23 | 24 | (note, if you were in the `frontend/` directory above then `cd ..` back to `sdk-examples/python/hackathon_app/`) 25 | 26 | ```sh 27 | brew install pyenv 28 | ``` 29 | 30 | **Important** [Follow these remaining steps (only 3 - 5)](https://github.com/pyenv/pyenv#basic-github-checkout) to complete the pyenv installation 31 | 32 | Now you’re ready to install python 3.7 and set it as your global python version for the following steps 33 | 34 | ```sh 35 | pyenv install 3.7.4 36 | pyenv global 3.7.4 37 | ``` 38 | 39 | hackathon_app uses [pipenv](https://docs.pipenv.org/en/latest/#install-pipenv-today) to manage project dependencies 40 | 41 | ```sh 42 | brew install pipenv 43 | ``` 44 | 45 | Install all dependencies. 46 | 47 | ```sh 48 | pipenv install --dev 49 | ``` 50 | 51 | #### Create sheets api creds 52 | 53 | Create a GCP project in your personal google console account: 54 | 55 | - enable Sheets api 56 | - enable Drive api 57 | - create a service account 58 | - download the .json creds file 59 | - copy the email address for the next step 60 | 61 | #### Setup `env.list` 62 | 63 | - Copy `env.list.sample` to `env.list` and modify accordingly (see instructions for base64 encoding your gcp .json credentials in the code comment for `GOOGLE_APPLICATION_CREDENTIAL_ENCODED`) 64 | - Leave `GOOGLE_SHEET_ID` alone for now, you'll add it after the next step 65 | 66 | #### Create a google spreadsheet 67 | 68 | ```sh 69 | ./create-test-sheet.sh 70 | ``` 71 | 72 | - go back to your spreadsheet and "share" it with the email address of the service account 73 | - Note the sheet ID (from the output) to use in the `env.list` file below. 74 | 75 | #### Run local dev servers 76 | 77 | ```sh 78 | ./start-dev-flask.sh 79 | ``` 80 | 81 | - this launches the backend server available at http://127.0.0.1:5000 82 | 83 | ```sh 84 | cd frontend/ 85 | yarn start 86 | ``` 87 | 88 | - this launches the frontend static server at http://127.0.0.1:3000 which proxies fetch requests to the backend server 89 | 90 | ### Build docker image 91 | 92 | Make sure to [install docker](https://download.docker.com/mac/stable/Docker.dmg) 93 | 94 | ```sh 95 | ./build-docker.sh 96 | ``` 97 | 98 | you can run the docker container locally with 99 | 100 | ```sh 101 | ./run-docker.sh 102 | ``` 103 | 104 | which exposes the whole app (both frontend and backend) on http://127.0.0.1:8080 105 | 106 | to stop: 107 | 108 | ```sh 109 | sh stop-docker.sh 110 | ``` 111 | 112 | # Hackathon email authentication 113 | 114 | Email authentication for the Hackathon app is supported via an email link with an auth code, and Google login. 115 | ![Hackitecture](hackitecture.jpg) 116 | -------------------------------------------------------------------------------- /python/hackathon_app/authentication.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import Optional 3 | 4 | from cryptography import fernet 5 | import sendgrid # type: ignore 6 | from typing_extensions import Protocol 7 | 8 | import sheets 9 | 10 | 11 | class PCrypto(Protocol): 12 | def encrypt(self, value: str) -> str: 13 | """Encrypt a string. 14 | 15 | :param value: string value to encrypt 16 | :return: the encrypted string 17 | """ 18 | ... 19 | 20 | def decrypt(self, value: str) -> str: 21 | """Decrypt a string. 22 | 23 | :param value: string value to decrypt 24 | :return: the decrypted string 25 | """ 26 | ... 27 | 28 | 29 | class Crypto: 30 | def __init__(self, key: str): 31 | self.cypher = fernet.Fernet(key.encode()) 32 | 33 | def encrypt(self, value: str) -> str: 34 | return self.cypher.encrypt(value.encode()).decode() 35 | 36 | def decrypt(self, value: str) -> str: 37 | return self.cypher.decrypt(value.encode()).decode() 38 | 39 | 40 | class PEmail(Protocol): 41 | def send(self, to_email: str, subject: str, body: str) -> None: 42 | ... 43 | 44 | 45 | class Email: 46 | def __init__(self, from_email: str, api_key: str): 47 | self.client = sendgrid.SendGridAPIClient(api_key) 48 | self.from_email = from_email 49 | 50 | def send(self, to_email: str, subject: str, body: str) -> None: 51 | message = sendgrid.Mail( 52 | from_email=self.from_email, 53 | to_emails=to_email, 54 | subject=subject, 55 | html_content=body, 56 | ) 57 | self.client.send(message) 58 | 59 | 60 | class Authentication: 61 | def __init__(self, *, crypto: PCrypto, sheet: sheets.Sheets, email: PEmail): 62 | self.crypto = crypto 63 | self.sheet = sheet 64 | self.email = email 65 | 66 | @classmethod 67 | def configure( 68 | cls, *, crypto_key: str, from_email: str, email_key: str, sheet: sheets.Sheets 69 | ) -> "Authentication": 70 | crypto = Crypto(crypto_key) 71 | email = Email(from_email, email_key) 72 | return cls(crypto=crypto, sheet=sheet, email=email) 73 | 74 | def auth_user(self, auth_code: str) -> Optional[sheets.User]: 75 | """Authenticate the user from the auth code 76 | 77 | :param value: string value of an auth code 78 | :return: a sheets.User or None if no corresponding user found 79 | """ 80 | token = self.crypto.decrypt(auth_code) 81 | email = token.split("~")[0] 82 | return self.sheet.users.find(email) 83 | 84 | def send_auth_message(self, user: sheets.User, host_url: str) -> None: 85 | """Send the email authentication link to the user. 86 | """ 87 | subject = "Welcome to the Looker Hackathon!" 88 | body = f"""

Welcome to the Looker Hackathon!

89 | Please click {host_url}auth/{self.get_user_auth_code(user)} to authenticate your email so you can use the Hackathon application 90 | and participate in the Hackathon 91 | """ 92 | self.email.send(user.email, subject, body) 93 | 94 | def get_user_auth_code(self, user: sheets.User) -> str: 95 | token = f"{user.email}~{datetime.datetime.now(tz=datetime.timezone.utc)}" 96 | return self.crypto.encrypt(token) 97 | -------------------------------------------------------------------------------- /python/hackathon_app/build-docker.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | cd frontend 3 | yarn install 4 | yarn build 5 | cd .. 6 | if [ -f status.json ]; then 7 | mv status.json frontend/build/ 8 | fi 9 | docker build -t hackathon_app . 10 | -------------------------------------------------------------------------------- /python/hackathon_app/bulk_import.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import csv 3 | import os 4 | import time 5 | 6 | import click 7 | 8 | import looker 9 | import sheets 10 | 11 | 12 | @click.command() 13 | @click.option("--filename", help="CSV file containing registrations") 14 | @click.option("--hackathon", help="Hackathon name") 15 | @click.option("--limit", help="limit current run", default=0) 16 | @click.option( 17 | "--enable/--no-enable", 18 | default=False, 19 | help="Enable user accounts in hack.looker.com", 20 | ) 21 | def main(filename: str, hackathon: str, enable: bool, limit: int): 22 | f = open(filename) 23 | registrants = csv.DictReader(f) 24 | 25 | sheets_client = sheets.Sheets( 26 | spreadsheet_id=os.environ["GOOGLE_SHEET_ID"], 27 | cred_file=os.environ["GOOGLE_APPLICATION_CREDENTIALS"], 28 | ) 29 | count = 0 30 | for registrant in registrants: 31 | registrant["hackathon"] = hackathon 32 | click.secho(f"Registering {registrant['email']}", fg="green") 33 | 34 | register_user = sheets.RegisterUser(**registrant) 35 | try: 36 | sheets_user = sheets_client.register_user(register_user) 37 | except sheets.SheetError as ex: 38 | click.secho( 39 | f"Failed to add to sheet. Stopping after {count} users", fg="red" 40 | ) 41 | f.close() 42 | raise ex 43 | else: 44 | try: 45 | client_id = looker.register_user( 46 | hackathon=hackathon, 47 | first_name=register_user.first_name, 48 | last_name=register_user.last_name, 49 | email=register_user.email, 50 | ) 51 | except looker.RegisterError as ex: 52 | f.close() 53 | click.secho( 54 | f"Failed to add to Looker. Stopping after {count} users", fg="red" 55 | ) 56 | raise ex 57 | sheets_user.client_id = client_id 58 | sheets_client.users.save(sheets_user) 59 | count += 1 60 | if limit and count == int(limit): 61 | break 62 | click.secho(f"Registered {count} users", fg="green") 63 | 64 | if enable: 65 | for email, reset in looker.enable_users_by_hackathons([hackathon]).items(): 66 | sheets_user = sheets_client.users.find(email) 67 | if not sheets_user: 68 | click.secho(f"Failed to find {email} in spreadsheet", fg="red") 69 | continue 70 | sheets_user.setup_link = reset 71 | sheets_client.users.save(sheets_user) 72 | time.sleep(1) 73 | click.secho(f"Enabled {count} users", fg="green") 74 | f.close() 75 | 76 | 77 | if __name__ == "__main__": 78 | google_creds = os.environ["GOOGLE_APPLICATION_CREDENTIAL_ENCODED"] 79 | assert google_creds 80 | with open("./google-creds.json", "wb") as f: 81 | f.write(base64.b64decode(google_creds)) 82 | main() 83 | -------------------------------------------------------------------------------- /python/hackathon_app/config.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import os 3 | 4 | 5 | SECRET_KEY = os.environ.get("FLASK_SECRET_KEY") 6 | WTF_CSRF_SECRET_KEY = os.environ.get("FLASK_WTF_CSRF_SECRET_KEY") 7 | 8 | FROM_EMAIL = os.environ.get("FROM_EMAIL") 9 | SENDGRID_API_KEY = os.environ.get("SENDGRID_API_KEY") 10 | 11 | GOOGLE_SHEET_ID = os.environ.get("GOOGLE_SHEET_ID") 12 | assert GOOGLE_SHEET_ID 13 | 14 | GOOGLE_APPLICATION_CREDENTIALS = os.environ.get("GOOGLE_APPLICATION_CREDENTIALS") 15 | assert GOOGLE_APPLICATION_CREDENTIALS 16 | # see https://github.com/looker/connection-hub/commit/df77b0ea606c4af8da9253759c5a0482f44342be 17 | # for why we're doing it this way 18 | _google_creds = os.environ.get("GOOGLE_APPLICATION_CREDENTIAL_ENCODED") 19 | assert _google_creds 20 | with open(GOOGLE_APPLICATION_CREDENTIALS, "wb") as f: 21 | f.write(base64.b64decode(_google_creds)) 22 | -------------------------------------------------------------------------------- /python/hackathon_app/create-test-sheet.py: -------------------------------------------------------------------------------- 1 | """Create a test sheet for local dev 2 | 3 | Execute with create-test-sheet.sh passing in your email address: 4 | 5 | ./create-test-sheet.sh joel.dodge@looker.com 6 | 7 | Mostly copied from tests/conftest.py with the addition of 8 | sharing/transfering ownership to you. 9 | 10 | """ 11 | import base64 12 | import json 13 | import os 14 | import sys 15 | 16 | from google.oauth2 import service_account # type: ignore 17 | from googleapiclient import discovery # type: ignore 18 | 19 | email = sys.argv[1] 20 | google_creds = os.environ.get("GOOGLE_APPLICATION_CREDENTIAL_ENCODED") 21 | assert google_creds 22 | with open("./google-creds.json", "wb") as f: 23 | f.write(base64.b64decode(google_creds)) 24 | scopes = [ 25 | "https://www.googleapis.com/auth/drive", 26 | "https://www.googleapis.com/auth/drive.file", 27 | "https://www.googleapis.com/auth/spreadsheets", 28 | ] 29 | 30 | credentials = service_account.Credentials.from_service_account_file( 31 | "google-creds.json", scopes=scopes 32 | ) 33 | 34 | os.remove("./google-creds.json") 35 | 36 | service = discovery.build("sheets", "v4", credentials=credentials) 37 | spreadsheet_client = service.spreadsheets() 38 | with open("tests/data/data.json", "r") as t: 39 | test_data = json.load(t) 40 | request = spreadsheet_client.create(body=test_data) 41 | response = request.execute() 42 | print(f"Created spreadsheet {response['spreadsheetId']}") 43 | drive_client = discovery.build("drive", "v3", credentials=credentials) 44 | 45 | 46 | def callback(request_id, response, exception): 47 | if exception: 48 | # Handle error 49 | raise exception 50 | else: 51 | print(f"Permission Id:{response.get('id')}") 52 | 53 | 54 | batch = drive_client.new_batch_http_request(callback=callback) 55 | user_permission = {"type": "user", "role": "owner", "emailAddress": email} 56 | batch.add( 57 | drive_client.permissions().create( 58 | transferOwnership=True, 59 | fileId=response["spreadsheetId"], 60 | body=user_permission, 61 | fields="id", 62 | ) 63 | ) 64 | batch.execute() 65 | print(f"Shared spreadsheet {response['spreadsheetId']} with {email}") 66 | -------------------------------------------------------------------------------- /python/hackathon_app/create-test-sheet.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | cd "$(dirname "$0")" 4 | ENV_LIST=./env.list 5 | if [ ! -f $ENV_LIST ]; then 6 | echo "Please create env.list from env.list.sample" 7 | exit 1 8 | fi 9 | source $ENV_LIST 10 | pipenv run python create-test-sheet.py "$1" 11 | -------------------------------------------------------------------------------- /python/hackathon_app/env.list.sample: -------------------------------------------------------------------------------- 1 | # Assign appropriate values here 2 | export LOOKERSDK_CLIENT_ID='yourclientid' 3 | export LOOKERSDK_CLIENT_SECRET='yourclientsecret' 4 | export GOOGLE_SHEET_ID='your-dev-google-sheet' 5 | 6 | # Hackathon app settings 7 | export SENDGRID_API_KEY='your sendgrid api key' 8 | export FROM_EMAIL='a valid sender email address' 9 | 10 | # Test settings 11 | export TEST_TO_EMAIL='a test email address' 12 | 13 | # For debugging support call breakpoint() in code when using start-dev-flash.sh 14 | PYTHONBREAKPOINT=pudb.set_trace 15 | 16 | 17 | # instructions: base64 encode the contents of your google .json file by running 18 | # "cat FILENAME | base64" and assign it here. It will be a really long string 19 | export GOOGLE_APPLICATION_CREDENTIAL_ENCODED='your-base64-encoded-json-creds-file-contents' 20 | 21 | 22 | 23 | # you can leave the rest as is for local dev 24 | export FLASK_APP='server.main' 25 | export FLASK_ENV='development' 26 | export FLASK_WTF_CSRF_SECRET_KEY='somesecretwtfkey' 27 | # cryptography.fernet.Fernet.generate_key().decode() 28 | export FLASK_SECRET_KEY='7JjMlzoHlVQ_KN-JTQdM5ciBnCTs46KQAcZVTc4dh1k=' 29 | export LOOKERSDK_API_VERSION='3.1' 30 | export LOOKERSDK_BASE_URL='https://self-signed.looker.com:19999' 31 | export LOOKERSDK_VERIFY_SSL='False' 32 | export GOOGLE_APPLICATION_CREDENTIALS='./google-creds.json' 33 | # avoid looker-sdk localhost warnings: InsecureRequestWarning: Unverified HTTPS request 34 | export PYTHONWARNINGS=ignore 35 | -------------------------------------------------------------------------------- /python/hackathon_app/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /python/hackathon_app/frontend/.nvmrc: -------------------------------------------------------------------------------- 1 | 12 2 | -------------------------------------------------------------------------------- /python/hackathon_app/frontend/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "bracketSpacing": false 6 | } 7 | -------------------------------------------------------------------------------- /python/hackathon_app/frontend/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `yarn start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `yarn test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `yarn build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `yarn eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | -------------------------------------------------------------------------------- /python/hackathon_app/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@looker/components": "0.7.23", 7 | "@reach/router": "^1.2.1", 8 | "@types/jest": "24.0.18", 9 | "@types/node": "12.7.12", 10 | "@types/reach__router": "^1.2.5", 11 | "@types/react": "16.9.5", 12 | "@types/react-dom": "16.9.1", 13 | "@types/styled-components": "^4.1.19", 14 | "@types/yup": "^0.26.24", 15 | "formik": "^1.5.8", 16 | "prettier": "^1.18.2", 17 | "react": "^16.10.2", 18 | "react-dom": "^16.10.2", 19 | "react-google-login": "^5.0.7", 20 | "react-scripts": "3.4.1", 21 | "styled-components": "^4.4.0", 22 | "typescript": "3.6.3", 23 | "yup": "^0.27.0" 24 | }, 25 | "scripts": { 26 | "start": "react-scripts start", 27 | "build": "react-scripts build", 28 | "test": "react-scripts test", 29 | "eject": "react-scripts eject" 30 | }, 31 | "eslintConfig": { 32 | "extends": "react-app" 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | }, 46 | "proxy": "http://localhost:5000", 47 | "homepage": "http://mywebsite.com/static" 48 | } 49 | -------------------------------------------------------------------------------- /python/hackathon_app/frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/looker-open-source/sdk-examples/a7eff0c2cd9724e8e548aba102b047e10a8dcbc3/python/hackathon_app/frontend/public/favicon.ico -------------------------------------------------------------------------------- /python/hackathon_app/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 19 | 23 | 32 | Hackathon Registration 33 | 34 | 35 | 36 |
37 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /python/hackathon_app/frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/looker-open-source/sdk-examples/a7eff0c2cd9724e8e548aba102b047e10a8dcbc3/python/hackathon_app/frontend/public/logo192.png -------------------------------------------------------------------------------- /python/hackathon_app/frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/looker-open-source/sdk-examples/a7eff0c2cd9724e8e548aba102b047e10a8dcbc3/python/hackathon_app/frontend/public/logo512.png -------------------------------------------------------------------------------- /python/hackathon_app/frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /python/hackathon_app/frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /python/hackathon_app/frontend/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | } 8 | 9 | .App-header { 10 | background-color: #282c34; 11 | min-height: 100vh; 12 | display: flex; 13 | flex-direction: column; 14 | align-items: center; 15 | justify-content: center; 16 | font-size: calc(10px + 2vmin); 17 | color: white; 18 | } 19 | 20 | .App-link { 21 | color: #09d3ac; 22 | } 23 | -------------------------------------------------------------------------------- /python/hackathon_app/frontend/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /python/hackathon_app/frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import {theme, GlobalStyle, Card, Flex} from '@looker/components' 2 | import {ThemeProvider} from 'styled-components' 3 | import * as React from 'react' 4 | import {Router} from '@reach/router' 5 | import {Logo} from './Logo' 6 | import {RegisterScene} from './RegisterScene' 7 | import {ResourcesScene} from './ResourcesScene' 8 | 9 | const App: React.FC = () => { 10 | return ( 11 | 12 | 13 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ) 31 | } 32 | export default App 33 | -------------------------------------------------------------------------------- /python/hackathon_app/frontend/src/Logo.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import Looker_Logo_Purple from './Looker_Logo_Purple.svg' 3 | 4 | export const Logo = styled.img.attrs({src: Looker_Logo_Purple})` 5 | max-width: 150px; 6 | margin: 0 0 20px; 7 | ` 8 | -------------------------------------------------------------------------------- /python/hackathon_app/frontend/src/Looker_Logo_Purple.svg: -------------------------------------------------------------------------------- 1 | Looker_Logo_Purple -------------------------------------------------------------------------------- /python/hackathon_app/frontend/src/ResourcesScene.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Button, Heading, Paragraph, Divider} from '@looker/components' 3 | 4 | export const ResourcesScene: React.FC<{path: string}> = ({path}) => { 5 | const [name, setName] = React.useState('') 6 | const personalizedText = name === '' ? '' : ` back ${name}` 7 | React.useEffect(() => { 8 | async function fetchData() { 9 | try { 10 | const response = await fetch('/user_info') 11 | if (response.ok) { 12 | const userData = await response.json() 13 | if (userData.first_name) { 14 | setName(`${userData.first_name} ${userData.last_name}`) 15 | } 16 | } else { 17 | console.log(response) 18 | } 19 | } catch (e) { 20 | console.log('Error fetching user:', e) 21 | } 22 | } 23 | fetchData() 24 | }, []) 25 | 26 | let message = `Welcome${personalizedText}!` 27 | if (path === '/thankyou') { 28 | message = `Thank you for registering` 29 | if (name !== '') { 30 | message = `${message}, ${name}!` 31 | } else { 32 | message = `${message}! Check your email for an authorization link.` 33 | } 34 | } 35 | 36 | return ( 37 | <> 38 | Looker Hackathons 39 | Find information on Hackathons below 40 | 41 | {message} 42 | 43 | Explore the links below to find useful documentation and tools for 44 | participating in a hackathon. 45 | 46 | 49 | 52 | 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /python/hackathon_app/frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /python/hackathon_app/frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /python/hackathon_app/frontend/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /python/hackathon_app/frontend/src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | (process as { env: { [key: string]: string } }).env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl) 112 | .then(response => { 113 | // Ensure service worker exists, and that we really are getting a JS file. 114 | const contentType = response.headers.get('content-type'); 115 | if ( 116 | response.status === 404 || 117 | (contentType != null && contentType.indexOf('javascript') === -1) 118 | ) { 119 | // No service worker found. Probably a different app. Reload the page. 120 | navigator.serviceWorker.ready.then(registration => { 121 | registration.unregister().then(() => { 122 | window.location.reload(); 123 | }); 124 | }); 125 | } else { 126 | // Service worker found. Proceed as normal. 127 | registerValidSW(swUrl, config); 128 | } 129 | }) 130 | .catch(() => { 131 | console.log( 132 | 'No internet connection found. App is running in offline mode.' 133 | ); 134 | }); 135 | } 136 | 137 | export function unregister() { 138 | if ('serviceWorker' in navigator) { 139 | navigator.serviceWorker.ready.then(registration => { 140 | registration.unregister(); 141 | }); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /python/hackathon_app/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /python/hackathon_app/hackitecture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/looker-open-source/sdk-examples/a7eff0c2cd9724e8e548aba102b047e10a8dcbc3/python/hackathon_app/hackitecture.jpg -------------------------------------------------------------------------------- /python/hackathon_app/logging.conf: -------------------------------------------------------------------------------- 1 | [loggers] 2 | keys = root 3 | 4 | [logger_root] 5 | level = INFO 6 | handlers = json 7 | 8 | [handlers] 9 | keys = json 10 | 11 | [handler_json] 12 | class = StreamHandler 13 | level = INFO 14 | formatter = json 15 | args = (sys.stdout,) 16 | 17 | [formatters] 18 | keys = json 19 | 20 | [formatter_json] 21 | format = %(message)s 22 | class = pythonjsonlogger.jsonlogger.JsonFormatter 23 | -------------------------------------------------------------------------------- /python/hackathon_app/looker.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import re 3 | from typing import Dict, Sequence 4 | 5 | import looker_sdk 6 | from looker_sdk import methods, models, error 7 | 8 | 9 | LOOKER_GROUP_PREFIX = "Looker_Hack: " 10 | # simple caching mechanism until we have a true class for retaining these IDs 11 | HACKATHON_ATTR_ID = None 12 | HACKATHON_ROLE = None 13 | 14 | 15 | def try_to(func): 16 | """Wrap API calls in try/except 17 | """ 18 | 19 | @functools.wraps(func) 20 | def wrapped_f(**kwargs): 21 | try: 22 | return func(**kwargs) 23 | except error.SDKError as ex: 24 | raise RegisterError(f"Failed to {func.__name__}: ({ex})") 25 | 26 | return wrapped_f 27 | 28 | 29 | @try_to 30 | def get_hackathon_attr_id(*, sdk: methods.LookerSDK) -> int: 31 | global HACKATHON_ATTR_ID 32 | if HACKATHON_ATTR_ID is not None: 33 | return HACKATHON_ATTR_ID 34 | 35 | main_hackathon = "hackathon" 36 | user_attrs = sdk.all_user_attributes(fields="name,id") 37 | for user_attr in user_attrs: 38 | if user_attr.name == main_hackathon: 39 | assert user_attr.id 40 | HACKATHON_ATTR_ID = user_attr.id 41 | break 42 | else: 43 | attrib = sdk.create_user_attribute( 44 | body=models.WriteUserAttribute( 45 | name=main_hackathon, label="Looker Hackathon", type="string" 46 | ) 47 | ) 48 | if not attrib: 49 | raise RegisterError(f"Could not find '{main_hackathon}' user attribute") 50 | else: 51 | assert attrib.id 52 | HACKATHON_ATTR_ID = attrib.id 53 | 54 | return HACKATHON_ATTR_ID 55 | 56 | 57 | @try_to 58 | def get_hackathon_role(*, sdk: methods.LookerSDK) -> models.Role: 59 | global HACKATHON_ROLE 60 | if HACKATHON_ROLE is not None: 61 | return HACKATHON_ROLE 62 | 63 | for role in sdk.all_roles(fields="name,id"): 64 | if role.name == "Hackathon": 65 | HACKATHON_ROLE = role 66 | assert HACKATHON_ROLE.id 67 | break 68 | else: 69 | raise RegisterError("Hackathon role needs to be created") 70 | 71 | return HACKATHON_ROLE 72 | 73 | 74 | def register_user( 75 | *, hackathon: str, first_name: str, last_name: str, email: str 76 | ) -> str: 77 | sdk = looker_sdk.init31() 78 | 79 | user = find_or_create_user( 80 | sdk=sdk, first_name=first_name, last_name=last_name, email=email 81 | ) 82 | assert user.id 83 | if not user.credentials_email: 84 | create_email_credentials(sdk=sdk, user_id=user.id, email=email) 85 | if user.credentials_api3: 86 | client_id = user.credentials_api3[0].client_id 87 | else: 88 | client_id = create_api3_credentials(sdk=sdk, user_id=user.id).client_id 89 | set_user_group(sdk=sdk, user_id=user.id, hackathon=hackathon) 90 | set_user_attributes(sdk=sdk, user_id=user.id, hackathon=hackathon) 91 | disable_user(sdk=sdk, user_id=user.id) 92 | assert client_id 93 | return client_id 94 | 95 | 96 | def find_or_create_user( 97 | *, sdk: methods.LookerSDK, first_name: str, last_name: str, email: str 98 | ) -> models.User: 99 | try: 100 | users = sdk.search_users(email=email) 101 | if users: 102 | user = users[0] 103 | if ( 104 | user.first_name != first_name 105 | or user.last_name != last_name 106 | or user.is_disabled 107 | ): 108 | assert user.id 109 | user = sdk.update_user( 110 | user_id=user.id, 111 | body=models.WriteUser( 112 | first_name=first_name, last_name=last_name, is_disabled=False 113 | ), 114 | ) 115 | else: 116 | user = sdk.create_user( 117 | models.WriteUser(first_name=first_name, last_name=last_name) 118 | ) 119 | except error.SDKError as create_ex: 120 | raise RegisterError(f"Failed to find or create User ({create_ex})") 121 | return user 122 | 123 | 124 | def enable_users_by_hackathons(hackathons: Sequence[str]) -> Dict[str, str]: 125 | global LOOKER_GROUP_PREFIX 126 | sdk = looker_sdk.init31() 127 | groups = {g.name: g.id for g in sdk.all_groups(fields="id,name")} 128 | ret = {} 129 | for hackathon in hackathons: 130 | try: 131 | group_id = groups[f"{LOOKER_GROUP_PREFIX}{hackathon}"] 132 | except KeyError: 133 | raise RegisterError(f"No group found for hackathon: '{hackathon}'") 134 | for user in sdk.search_users(group_id=group_id): 135 | assert user.id 136 | assert user.email 137 | sdk.update_user(user_id=user.id, body=models.WriteUser(is_disabled=False)) 138 | password_reset_url = sdk.create_user_credentials_email_password_reset( 139 | user_id=user.id, expires=False 140 | ).password_reset_url 141 | assert password_reset_url 142 | setup = re.sub("password/reset", "account/setup", password_reset_url) 143 | ret[user.email] = setup 144 | return ret 145 | 146 | 147 | @try_to 148 | def create_email_credentials(*, sdk: methods.LookerSDK, user_id: int, email: str): 149 | sdk.create_user_credentials_email( 150 | user_id=user_id, body=models.WriteCredentialsEmail(email=email) 151 | ) 152 | 153 | 154 | @try_to 155 | def create_api3_credentials( 156 | *, sdk: methods.LookerSDK, user_id: int 157 | ) -> models.CredentialsApi3: 158 | return sdk.create_user_credentials_api3( 159 | user_id=user_id, body=models.CredentialsApi3() 160 | ) 161 | 162 | 163 | @try_to 164 | def set_user_group(*, sdk: methods.LookerSDK, user_id: int, hackathon: str): 165 | global LOOKER_GROUP_PREFIX 166 | # TODO - switch to sdk.search_groups once that method is live on 167 | # sandboxcl and hack instances 168 | groups = sdk.all_groups(fields="id,name") 169 | name = f"{LOOKER_GROUP_PREFIX}{hackathon}" 170 | for group in groups: 171 | if group.name == name: 172 | break 173 | else: 174 | role = get_hackathon_role(sdk=sdk) 175 | assert role.id 176 | role_groups = [] 177 | for g in sdk.role_groups(role_id=role.id, fields="id"): 178 | assert g.id 179 | role_groups.append(g.id) 180 | group = sdk.create_group(body=models.WriteGroup(name=name)) 181 | assert group.id 182 | role_groups.append(group.id) 183 | sdk.set_role_groups(role_id=role.id, body=role_groups) 184 | 185 | assert group.id 186 | sdk.add_group_user( 187 | group_id=group.id, body=models.GroupIdForGroupUserInclusion(user_id=user_id) 188 | ) 189 | 190 | 191 | @try_to 192 | def set_user_attributes(*, sdk: methods.LookerSDK, user_id, hackathon): 193 | hackathon_attr_id = get_hackathon_attr_id(sdk=sdk) 194 | assert hackathon_attr_id 195 | sdk.set_user_attribute_user_value( 196 | user_id=user_id, 197 | user_attribute_id=hackathon_attr_id, 198 | body=models.WriteUserAttributeWithValue(value=hackathon), 199 | ) 200 | 201 | 202 | @try_to 203 | def disable_user(*, sdk: methods.LookerSDK, user_id: int): 204 | sdk.update_user(user_id=user_id, body=models.WriteUser(is_disabled=True)) 205 | 206 | 207 | def me(): 208 | sdk = looker_sdk.init31() 209 | return sdk.me() 210 | 211 | 212 | class RegisterError(Exception): 213 | """Failed to register user in looker instance. 214 | """ 215 | -------------------------------------------------------------------------------- /python/hackathon_app/mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | python_version=3.7 3 | no_implicit_optional=True 4 | 5 | 6 | [mypy-tests.*] 7 | disallow_any_expr=False 8 | -------------------------------------------------------------------------------- /python/hackathon_app/run-docker.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | docker run -d --name hackathon_app_container -p 8080:80 hackathon_app 3 | docker logs -f hackathon_app_container 4 | -------------------------------------------------------------------------------- /python/hackathon_app/run-tests.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | cd "$(dirname "$0")" 4 | ENV_LIST=./env.list 5 | if [ ! -f $ENV_LIST ]; then 6 | echo "Please create env.list from env.list.sample" 7 | exit 1 8 | fi 9 | 10 | if [ $# -eq 0 ]; then 11 | ARGS=tests/ 12 | 13 | # if any arguments are passed, they need to include the path to tests/ 14 | else 15 | ARGS="$@" 16 | shift 17 | fi 18 | 19 | source $ENV_LIST 20 | pipenv run py.test $ARGS 21 | -------------------------------------------------------------------------------- /python/hackathon_app/server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/looker-open-source/sdk-examples/a7eff0c2cd9724e8e548aba102b047e10a8dcbc3/python/hackathon_app/server/__init__.py -------------------------------------------------------------------------------- /python/hackathon_app/start-dev-flask.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | cd "$(dirname "$0")" 3 | ENV_LIST=./env.list 4 | if [ ! -f $ENV_LIST ]; then 5 | echo "Please create env.list from env.list.sample" 6 | exit 1 7 | fi 8 | source $ENV_LIST 9 | export FLASK_APP="server.main" 10 | export FLASK_ENV="development" 11 | pipenv run flask run 12 | -------------------------------------------------------------------------------- /python/hackathon_app/stop-docker.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | cd "$(dirname "$0")" 3 | docker stop hackathon_app_container && docker rm hackathon_app_container 4 | -------------------------------------------------------------------------------- /python/hackathon_app/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/looker-open-source/sdk-examples/a7eff0c2cd9724e8e548aba102b047e10a8dcbc3/python/hackathon_app/tests/__init__.py -------------------------------------------------------------------------------- /python/hackathon_app/tests/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/looker-open-source/sdk-examples/a7eff0c2cd9724e8e548aba102b047e10a8dcbc3/python/hackathon_app/tests/integration/__init__.py -------------------------------------------------------------------------------- /python/hackathon_app/tests/integration/test_hackathons.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import List 3 | 4 | import sheets 5 | 6 | 7 | def test_rows_returns_hackathons( 8 | hackathons: sheets.Hackathons, test_hackathons: List[sheets.Hackathon] 9 | ): 10 | """rows() should return a list of Hackathon objects""" 11 | all_hackathons = hackathons.rows() 12 | assert isinstance(all_hackathons, list) 13 | assert len(all_hackathons) == len(test_hackathons) 14 | 15 | hackathon = all_hackathons[0] 16 | expected = test_hackathons[0] 17 | assert hackathon == expected 18 | 19 | 20 | def test_get_upcoming_hackathons( 21 | hackathons: sheets.Hackathons, test_hackathons: List[sheets.Hackathon] 22 | ): 23 | now = datetime.datetime.now(tz=datetime.timezone.utc) 24 | up_coming_expected = sorted( 25 | (h for h in test_hackathons if h.date >= now), key=lambda ht: ht.id 26 | ) 27 | up_coming_actual = sorted(hackathons.get_upcoming(), key=lambda ht: ht.id) 28 | assert up_coming_actual == up_coming_expected 29 | 30 | 31 | def test_get_upcoming_hackathons_with_cutoff( 32 | hackathons: sheets.Hackathons, test_hackathons: List[sheets.Hackathon] 33 | ): 34 | now = datetime.datetime.now(tz=datetime.timezone.utc) 35 | cutoff = now + datetime.timedelta(days=60) 36 | 37 | # setup some hackathons to occur within cutoff date 38 | # ensure we at most 3 hackathons so we don't decrement date into the past 39 | up_coming_expected = sorted( 40 | (h for h in test_hackathons if h.date >= now), key=lambda ht: ht.id 41 | )[:3] 42 | for d, hackathon in enumerate(up_coming_expected, start=1): 43 | hackathon.date = cutoff - datetime.timedelta(days=2 * d) 44 | hackathons.update(hackathon) 45 | assert len(up_coming_expected) > 0, "Bad fixture data setup" 46 | 47 | up_coming_actual = sorted( 48 | hackathons.get_upcoming(cutoff=cutoff), key=lambda ht: ht.id 49 | ) 50 | assert len(up_coming_actual) > 0 51 | for hackathon in up_coming_actual: 52 | assert hackathon.date < cutoff 53 | -------------------------------------------------------------------------------- /python/hackathon_app/tests/integration/test_looker.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import looker_sdk 4 | from looker_sdk import models, methods 5 | import pytest # type: ignore 6 | 7 | import looker 8 | import sheets 9 | 10 | 11 | @pytest.fixture 12 | def sdk(): 13 | sdk = looker_sdk.init31() 14 | yield sdk 15 | sdk.logout() 16 | 17 | 18 | @pytest.fixture 19 | def looker_test_users(sdk: methods.LookerSDK, test_users: List[sheets.User]): 20 | yield test_users 21 | for test_user in test_users: 22 | users = sdk.search_users(email=test_user.email) 23 | if len(users) > 0: 24 | assert users[0].id 25 | sdk.delete_user(user_id=users[0].id) 26 | 27 | 28 | @pytest.mark.parametrize("register_twice", [False, True]) 29 | def test_register_user( 30 | looker_test_users: List[sheets.User], sdk: methods.LookerSDK, register_twice: bool 31 | ): 32 | 33 | test_hackathon = "Some Hackathon" 34 | 35 | test_user = looker_test_users[0] 36 | looker.register_user( 37 | hackathon=test_hackathon, 38 | first_name=test_user.first_name, 39 | last_name=test_user.last_name, 40 | email=test_user.email, 41 | ) 42 | if register_twice: 43 | looker.register_user( 44 | hackathon=test_hackathon, 45 | first_name=test_user.first_name, 46 | last_name=test_user.last_name, 47 | email=test_user.email, 48 | ) 49 | 50 | users = sdk.search_users(email=test_user.email) 51 | assert len(users) > 0 52 | actual_user = users[0] 53 | 54 | assert actual_user.first_name == test_user.first_name 55 | assert actual_user.last_name == test_user.last_name 56 | assert actual_user.credentials_email 57 | assert actual_user.credentials_api3 58 | assert len(actual_user.credentials_api3) == 1 59 | assert actual_user.group_ids 60 | assert len(actual_user.group_ids) == 2 61 | assert actual_user.is_disabled 62 | 63 | groups = sdk.all_groups(ids=models.DelimSequence(actual_user.group_ids)) 64 | for group in groups: 65 | if group.name == f"Looker_Hack: {test_hackathon}": 66 | break 67 | else: 68 | pytest.fail(f"Failed to find or create 'Looker_Hack: {test_hackathon}'") 69 | for role in sdk.all_roles(fields="name,id"): 70 | if role.name == "Hackathon": 71 | break 72 | else: 73 | pytest.fail("Bad test setup, failed to find 'Hackathon' role") 74 | assert role.id 75 | role_groups = sdk.role_groups(role_id=role.id, fields="id") 76 | for role_group in role_groups: 77 | if role_group.id == group.id: 78 | break 79 | else: 80 | pytest.fail( 81 | f"Failed to assign group 'Looker_Hack: {test_hackathon}' to role 'Hackathon'" 82 | ) 83 | 84 | assert actual_user.id 85 | actual_attributes = sdk.user_attribute_user_values(user_id=actual_user.id) 86 | assert actual_attributes 87 | for actual_attribute in actual_attributes: 88 | if actual_attribute.name == "hackathon": 89 | assert actual_attribute.value == test_hackathon 90 | break 91 | else: 92 | assert False, "Not assigned hackathon role" 93 | 94 | 95 | def test_enable_users_by_hackathons( 96 | looker_test_users: List[sheets.User], sdk: methods.LookerSDK 97 | ): 98 | test_user1, test_user2, test_user3, test_user4 = looker_test_users 99 | looker.register_user( 100 | hackathon="hack_1", 101 | first_name=test_user1.first_name, 102 | last_name=test_user1.last_name, 103 | email=test_user1.email, 104 | ) 105 | looker.register_user( 106 | hackathon="hack_2", 107 | first_name=test_user2.first_name, 108 | last_name=test_user2.last_name, 109 | email=test_user2.email, 110 | ) 111 | looker.register_user( 112 | hackathon="hack_1", 113 | first_name=test_user3.first_name, 114 | last_name=test_user3.last_name, 115 | email=test_user3.email, 116 | ) 117 | looker.register_user( 118 | hackathon="hack_2", 119 | first_name=test_user4.first_name, 120 | last_name=test_user4.last_name, 121 | email=test_user4.email, 122 | ) 123 | 124 | assert sdk.search_users(fields="is_disabled", email=test_user1.email)[0].is_disabled 125 | assert sdk.search_users(fields="is_disabled", email=test_user2.email)[0].is_disabled 126 | assert sdk.search_users(fields="is_disabled", email=test_user3.email)[0].is_disabled 127 | assert sdk.search_users(fields="is_disabled", email=test_user4.email)[0].is_disabled 128 | 129 | looker.enable_users_by_hackathons(hackathons=["hack_1", "hack_2"]) 130 | 131 | assert not sdk.search_users(fields="is_disabled", email=test_user1.email)[ 132 | 0 133 | ].is_disabled 134 | assert not sdk.search_users(fields="is_disabled", email=test_user2.email)[ 135 | 0 136 | ].is_disabled 137 | assert not sdk.search_users(fields="is_disabled", email=test_user3.email)[ 138 | 0 139 | ].is_disabled 140 | assert not sdk.search_users(fields="is_disabled", email=test_user4.email)[ 141 | 0 142 | ].is_disabled 143 | -------------------------------------------------------------------------------- /python/hackathon_app/tests/integration/test_registrations.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from sheets import Registrant, Registrations 4 | 5 | DATE_FORMAT = "%m/%d/%Y" 6 | 7 | 8 | def test_rows_returns_registrants(registrations: Registrations, test_registrants): 9 | """rows() should return a list of Registrant objects""" 10 | all_registrations = registrations.rows() 11 | assert isinstance(all_registrations, list) 12 | assert len(all_registrations) == len(test_registrants) 13 | 14 | registrant = all_registrations[0] 15 | expected = test_registrants[0] 16 | assert registrant == expected 17 | 18 | 19 | def test_is_registered_returns_true_for_existing_registrants( 20 | registrations: Registrations, test_registrants 21 | ): 22 | """is_registered(registrant) should return True for already registered users""" 23 | registrant = test_registrants[0] 24 | existing_registrant = Registrant( 25 | user_email=registrant.user_email, 26 | hackathon_name=registrant.hackathon_name, 27 | date_registered=registrant.date_registered, 28 | attended=bool(registrant.attended), 29 | ) 30 | assert registrations.is_registered(existing_registrant) 31 | 32 | 33 | def test_is_registered_returns_false_for_new_registrants( 34 | registrations: Registrations, test_registrants 35 | ): 36 | """is_registered(registrant) should return False for already registered users""" 37 | new_registrant = Registrant( 38 | user_email="newregistrant@newompany.com", 39 | hackathon_name="brand_new_hackathon", 40 | date_registered=datetime.date.today(), 41 | attended=None, 42 | ) 43 | assert not registrations.is_registered(new_registrant) 44 | 45 | 46 | def test_register(registrations: Registrations): 47 | """register() should append new registrants to registrations sheets""" 48 | new_registrant = Registrant( 49 | user_email="newregistrant@newompany.com", 50 | hackathon_name="brand_new_hackathon", 51 | date_registered=datetime.datetime.now(), 52 | attended=None, 53 | ) 54 | assert not registrations.is_registered(new_registrant) 55 | registrations.register(new_registrant) 56 | assert registrations.is_registered(new_registrant) 57 | -------------------------------------------------------------------------------- /python/hackathon_app/tests/integration/test_sheets.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import Sequence 3 | 4 | from sheets import User, Users, RegisterUser, Registrant, Registrations, Sheets 5 | 6 | 7 | def test_gets_all_hackathons(sheets: Sheets, test_data): 8 | """get_hackathons() should return all active hackathons.""" 9 | hackathons = sheets.get_hackathons() 10 | assert isinstance(hackathons, list) 11 | assert len(hackathons) > 0 12 | 13 | 14 | def test_register_user_registers( 15 | sheets: Sheets, users: Users, registrations: Registrations, test_data 16 | ): 17 | """register_user() should register new users by adding them to the Users sheet 18 | and to the Registrations sheet 19 | """ 20 | new_user = sheets.register_user( 21 | RegisterUser( 22 | hackathon="sanfrancisco_2019", 23 | first_name="New", 24 | last_name="Registrant", 25 | email="newregistrant@newompany.com", 26 | organization="New Company", 27 | role="Data person", 28 | tshirt_size="M", 29 | ) 30 | ) 31 | 32 | all_users = users.rows() 33 | last_inserted_user = all_users[-1] 34 | assert new_user == last_inserted_user 35 | 36 | all_registrants = registrations.rows() 37 | last_registrant = all_registrants[-1] 38 | assert last_registrant.user_email == new_user.email 39 | assert last_registrant.hackathon_name == "sanfrancisco_2019" 40 | assert last_registrant.date_registered 41 | assert ( 42 | last_registrant.date_registered.date() 43 | == datetime.datetime.now(tz=datetime.timezone.utc).date() 44 | ) 45 | assert last_registrant.attended is None 46 | 47 | 48 | def test_register_user_registers_when_user_exists( 49 | test_users: Sequence[User], 50 | sheets: Sheets, 51 | users: Users, 52 | registrations: Registrations, 53 | ): 54 | """register_user() should register a user by adding them to the Registrations sheet 55 | if user already exists in the Users sheet but not in the Registrations sheet. 56 | """ 57 | existing_user = test_users[0] 58 | registered_user = sheets.register_user( 59 | RegisterUser( 60 | hackathon="newhackathon_2019", 61 | first_name=existing_user.first_name, 62 | last_name=existing_user.last_name, 63 | email=existing_user.email, 64 | organization=existing_user.organization, 65 | role=existing_user.role, 66 | tshirt_size=existing_user.tshirt_size, 67 | ) 68 | ) 69 | 70 | assert registered_user == existing_user 71 | all_users = sorted(users.rows(), key=lambda a: a.id) 72 | test_users = sorted(test_users, key=lambda t: t.id) 73 | assert all_users == test_users 74 | 75 | all_registrants = registrations.rows() 76 | last_registrant = all_registrants[-1] 77 | assert last_registrant.user_email == existing_user.email 78 | assert last_registrant.hackathon_name == "newhackathon_2019" 79 | assert last_registrant.date_registered 80 | assert ( 81 | last_registrant.date_registered.date() 82 | == datetime.datetime.now(tz=datetime.timezone.utc).date() 83 | ) 84 | assert last_registrant.attended is None 85 | 86 | 87 | def test_register_updates_user_if_user_is_registered( 88 | users: Users, 89 | test_users: Sequence[User], 90 | test_registrants: Sequence[Registrant], 91 | sheets: Sheets, 92 | registrations: Registrations, 93 | ): 94 | """register_user() should update the user but not re-register if user is already registered""" 95 | existing_registrant = test_registrants[0] 96 | for user in test_users: 97 | if user.email == existing_registrant.user_email: 98 | updated_user = user 99 | break 100 | 101 | updated_user.first_name = "updated_first" 102 | updated_user.last_name = "updated_last" 103 | updated_user.organization = "updated_org" 104 | updated_user.tshirt_size = "update_size" 105 | 106 | sheets.register_user( 107 | RegisterUser( 108 | hackathon="newhackathon_2019", 109 | first_name=updated_user.first_name, 110 | last_name=updated_user.last_name, 111 | email=updated_user.email, 112 | organization=updated_user.organization, 113 | role=updated_user.role, 114 | tshirt_size=updated_user.tshirt_size, 115 | ) 116 | ) 117 | 118 | retrieved_user = users.find(existing_registrant.user_email) 119 | assert retrieved_user == updated_user 120 | -------------------------------------------------------------------------------- /python/hackathon_app/tests/integration/test_users.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from sheets import User, Users 4 | 5 | 6 | def test_rows_returns_users(users: Users, test_users): 7 | """rows() should return a list of User objects""" 8 | all_users = users.rows() 9 | assert isinstance(all_users, list) 10 | assert len(all_users) == len(test_users) 11 | 12 | user = all_users[0] 13 | expected = test_users[0] 14 | assert user == expected 15 | 16 | 17 | def test_find_returns_existing_user(users: Users, test_users): 18 | """find(user) returns True if user already exists""" 19 | expected_user = test_users[0] 20 | actual_user = users.find(expected_user.email) 21 | assert actual_user == expected_user 22 | 23 | 24 | def test_find_returns_false_non_existant_user(users: Users): 25 | """find(user) returns False for new users""" 26 | assert not users.find("no-one-has-this-email") 27 | 28 | 29 | def test_create_user(users: Users): 30 | """create(user) should add a user to the users sheet""" 31 | new_user = User( 32 | first_name="Hundy", 33 | last_name="P", 34 | email="hundyp@company.com", 35 | organization="company", 36 | role="BI analyst", 37 | tshirt_size="M", 38 | ) 39 | users.save(new_user) 40 | all_users = users.rows() 41 | user = all_users[-1] 42 | assert user == new_user 43 | assert user.date_created < datetime.datetime.now(tz=datetime.timezone.utc) 44 | 45 | 46 | def test_update_user_updates(users: Users): 47 | """update(user) should modify existing users in the users sheet. The user's 48 | email is used to uniquely identify a user and cannot be amended from the front end. 49 | """ 50 | all_users = users.rows() 51 | updated_user = all_users[0] 52 | updated_user.first_name = "updated_first" 53 | updated_user.last_name = "updated_last" 54 | updated_user.organization = "updated_org" 55 | updated_user.organization = "updated_role" 56 | updated_user.tshirt_size = "update_size" 57 | users.save(updated_user) 58 | 59 | user = users.find(updated_user.email) 60 | assert user == updated_user 61 | -------------------------------------------------------------------------------- /python/hackathon_app/tests/unit/test_authentication.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import List 3 | 4 | from cryptography import fernet 5 | import pytest # type: ignore 6 | 7 | import authentication 8 | from sheets import Sheets, User 9 | 10 | 11 | class NoopCrypto: 12 | def encrypt(self, value: str) -> str: 13 | return value 14 | 15 | def decrypt(self, value: str) -> str: 16 | return value 17 | 18 | 19 | class NoopEmail: 20 | def send(self, to_email: str, subject: str, body: str) -> None: 21 | pass 22 | 23 | 24 | def test_send_email(): 25 | to_email = os.environ.get("TEST_TO_EMAIL") 26 | from_email = os.environ.get("FROM_EMAIL") 27 | api_key = os.environ.get("SENDGRID_API_KEY") 28 | if not (to_email and from_email and api_key): 29 | pytest.fail("Missing environment variables") 30 | 31 | emailer = authentication.Email(from_email, api_key) 32 | emailer.send(to_email, "hackathon app test", "this is a body") 33 | 34 | 35 | def test_encrypt_decrypt(): 36 | crypto = authentication.Crypto(fernet.Fernet.generate_key().decode()) 37 | value = "foobar" 38 | encrypted = crypto.encrypt(value) 39 | assert encrypted != value 40 | decrypted = crypto.decrypt(encrypted) 41 | assert decrypted == value 42 | 43 | 44 | def test_get_user_auth_code(sheets: Sheets): 45 | user = User( 46 | first_name="Hundy", 47 | last_name="P", 48 | email="hundyp@company.com", 49 | organization="company", 50 | role="BI analyst", 51 | tshirt_size="M", 52 | ) 53 | 54 | auth_service = authentication.Authentication( 55 | crypto=NoopCrypto(), sheet=sheets, email=NoopEmail() 56 | ) 57 | auth_code = auth_service.get_user_auth_code(user) 58 | email, date = auth_code.split("~") 59 | assert email == "hundyp@company.com" 60 | 61 | 62 | def test_auth_user(sheets: Sheets, test_users: List[User]): 63 | auth_service = authentication.Authentication( 64 | crypto=NoopCrypto(), sheet=sheets, email=NoopEmail() 65 | ) 66 | user = test_users[0] 67 | auth_code = auth_service.get_user_auth_code(user) 68 | authenticated_user = auth_service.auth_user(auth_code) 69 | assert authenticated_user == user 70 | 71 | 72 | def test_send_auth_message(sheets: Sheets, test_users: List[User]): 73 | user = test_users[0] 74 | 75 | class FooCrypto: 76 | def encrypt(self, value: str) -> str: 77 | return "foo" 78 | 79 | def decrypt(self, value: str) -> str: 80 | return "foo" 81 | 82 | class TestEmail: 83 | def __init__(self, to_email): 84 | self.to_email = user.email 85 | 86 | def send(self, to_email: str, subject: str, body: str) -> None: 87 | assert to_email == self.to_email 88 | assert subject == "Welcome to the Looker Hackathon!" 89 | assert ( 90 | body 91 | == f"""

Welcome to the Looker Hackathon!

92 | Please click https://foo.com/auth/foo to authenticate your email so you can use the Hackathon application 93 | and participate in the Hackathon 94 | """ 95 | ) 96 | 97 | auth_service = authentication.Authentication( 98 | crypto=FooCrypto(), sheet=sheets, email=TestEmail(user.email) 99 | ) 100 | auth_service.send_auth_message(user, "https://foo.com/") 101 | -------------------------------------------------------------------------------- /python/hackathon_app/tests/unit/test_wholly_sheet.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import pytest # type: ignore 3 | 4 | 5 | from sheets import WhollySheet, DATE_FORMAT 6 | 7 | 8 | def test_convert_to_list_returns_list(WhollySheet: WhollySheet): 9 | """_convert_to_list() should convert a dict into a list""" 10 | data = { 11 | "id": 5, 12 | "first_name": "John", 13 | "last_name": "Doe", 14 | "email": "john@foo.com", 15 | "created_date": datetime.date.strftime(datetime.date.today(), DATE_FORMAT), 16 | "organization": "foo", 17 | "tshirt_size": "S", 18 | } 19 | result = WhollySheet._convert_to_list(data) 20 | assert isinstance(result, list) 21 | assert result == [ 22 | "John", 23 | "Doe", 24 | "john@foo.com", 25 | datetime.date.strftime(datetime.date.today(), DATE_FORMAT), 26 | "foo", 27 | "S", 28 | ] 29 | 30 | 31 | @pytest.mark.parametrize( 32 | "test_input, expected_output", 33 | [ 34 | ( 35 | [ 36 | ["user_email", "hackathon_name", "date_registered", "attended"], 37 | ["jane@bar.com", "london2019", "6/10/2019", "True"], 38 | ["john@foo.com", "london2019", "6/11/2019", "False"], 39 | ], 40 | [ 41 | { 42 | "id": 2, 43 | "user_email": "jane@bar.com", 44 | "hackathon_name": "london2019", 45 | "date_registered": "6/10/2019", 46 | "attended": "True", 47 | }, 48 | { 49 | "id": 3, 50 | "user_email": "john@foo.com", 51 | "hackathon_name": "london2019", 52 | "date_registered": "6/11/2019", 53 | "attended": "False", 54 | }, 55 | ], 56 | ) 57 | ], 58 | ) 59 | def test_convert_to_dict_returns_dict( 60 | WhollySheet: WhollySheet, test_input, expected_output 61 | ): 62 | """_convert_to_dict() should convert a list of lists into a dictionary""" 63 | result = WhollySheet._convert_to_dict(test_input) 64 | assert result == expected_output 65 | -------------------------------------------------------------------------------- /python/logout_all_users.py: -------------------------------------------------------------------------------- 1 | from typing import cast, Sequence 2 | 3 | import looker_sdk 4 | from looker_sdk import models 5 | 6 | 7 | sdk = looker_sdk.init31("../looker.ini") 8 | 9 | 10 | def main(): 11 | users = get_all_users() 12 | 13 | if not users: 14 | print("No users found.") 15 | return 16 | 17 | count = 0 18 | for user in users: 19 | if user.sessions: 20 | terminate_sessions(cast(int, user.id), user.sessions) 21 | count += len(user.sessions) 22 | 23 | print(f"Terminated {count} session{'' if count == 1 else 's'}") 24 | 25 | 26 | def get_all_users() -> Sequence[models.User]: 27 | """Return a list of users.""" 28 | users = sdk.all_users(fields="id, sessions") 29 | return users 30 | 31 | 32 | def terminate_sessions(user_id: int, sessions: Sequence[models.Session]): 33 | """Delete active sessions for a given user id.""" 34 | for session in sessions: 35 | assert isinstance(session.id, int) 36 | sdk.delete_user_session(user_id, session.id) 37 | 38 | 39 | main() 40 | -------------------------------------------------------------------------------- /python/lookml_model_explore.py: -------------------------------------------------------------------------------- 1 | """Port of ruby "Creating a Data Dictionary" 2 | 3 | https://github.com/llooker/powered_by_modules/blob/master/Use%20Cases/Data%20Dictionary.md 4 | """ 5 | 6 | import looker_sdk 7 | 8 | 9 | def get_field_values(model_name, explore_name): 10 | 11 | sdk = looker_sdk.init31() 12 | 13 | # API Call to pull in metadata about fields in a particular explore 14 | explore = sdk.lookml_model_explore( 15 | lookml_model_name=model_name, 16 | explore_name=explore_name, 17 | fields="id, name, description, fields", 18 | ) 19 | 20 | my_fields = [] 21 | 22 | # Iterate through the field definitions and pull in the description, sql, 23 | # and other looker tags you might want to include in your data dictionary. 24 | if explore.fields and explore.fields.dimensions: 25 | for dimension in explore.fields.dimensions: 26 | dim_def = { 27 | "field_type": "Dimension", 28 | "view_name": dimension.view_label, 29 | "field_name": dimension.label_short, 30 | "type": dimension.type, 31 | "description": dimension.description, 32 | "sql": dimension.sql, 33 | } 34 | my_fields.append(dim_def) 35 | if explore.fields and explore.fields.measures: 36 | for measure in explore.fields.measures: 37 | mes_def = { 38 | "field_type": "Measure", 39 | "view_name": measure.view_label, 40 | "field_name": measure.label_short, 41 | "type": measure.type, 42 | "description": measure.description, 43 | "sql": measure.sql, 44 | } 45 | my_fields.append(mes_def) 46 | 47 | return my_fields 48 | -------------------------------------------------------------------------------- /python/mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | python_version=3.7 3 | 4 | check_untyped_defs=True 5 | -------------------------------------------------------------------------------- /python/oauth_session.py: -------------------------------------------------------------------------------- 1 | import getpass 2 | import sys 3 | import urllib.parse 4 | 5 | import requests 6 | 7 | from looker_sdk.rtl import api_settings, auth_session, requests_transport 8 | from looker_sdk.sdk.api31 import methods 9 | from looker_sdk.rtl import serialize 10 | 11 | 12 | def oauth_interactive_roundtrip(): 13 | username = input("\nemail: ") 14 | password = getpass.getpass() 15 | settings = api_settings.ApiSettings() 16 | transport = requests_transport.RequestsTransport.configure(settings) 17 | session = auth_session.OAuthSession( 18 | settings=settings, 19 | transport=transport, 20 | deserialize=serialize.deserialize31, 21 | serialize=serialize.serialize, 22 | crypto=auth_session.CryptoHash(), 23 | version="3.1", 24 | ) 25 | auth_code_request_url = session.create_auth_code_request_url("api", "mystate") 26 | with requests.Session() as s: 27 | s.verify = False 28 | 29 | redirect_to_login = s.get(auth_code_request_url) 30 | csrf_token = urllib.parse.unquote(s.cookies["CSRF-TOKEN"]) 31 | redirect = s.post( 32 | redirect_to_login.url, 33 | data={"csrf-token": csrf_token, "email": username, "password": password}, 34 | allow_redirects=False, 35 | ) 36 | assert redirect.next.path_url != "/login", "Failed to login to looker." 37 | 38 | while redirect.next: 39 | try: 40 | # 1. redirect to /auth 41 | # 2. (if already authorized app) redirect to settings.redirect_uri 42 | redirect = s.send(redirect.next, allow_redirects=False, timeout=2) 43 | except requests.exceptions.ConnectTimeout: 44 | # yep, redirected to settings.redirect_uri which doesn't exist hence 45 | # the timeout error. skip the app approval else block below 46 | break 47 | else: 48 | # redirected to app approval 49 | redirect = s.post( 50 | redirect.url, 51 | data={"csrf-token": csrf_token, "approve": "true"}, 52 | allow_redirects=False, 53 | ) 54 | assert redirect.status_code != 403, "User not allowed to authorize app!" 55 | 56 | # whether we had to authorize or were already authorized, we've now 57 | # been redirected to settings.redirect_uri, grab the Location header 58 | # query string to parse out the "code" 59 | qs = urllib.parse.urlparse(redirect.headers["Location"]).query 60 | params = urllib.parse.parse_qs(qs) 61 | assert "code" in params 62 | session.redeem_auth_code(params["code"][0]) 63 | sdk = methods.Looker31SDK( 64 | session, serialize.deserialize31, serialize.serialize, transport, "3.1" 65 | ) 66 | me = sdk.me() 67 | print(f"Hi {me.first_name}, your user_id is {me.id}") 68 | 69 | 70 | if __name__ == "__main__": 71 | sys.exit(oauth_interactive_roundtrip()) 72 | -------------------------------------------------------------------------------- /python/run_look_with_filters.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | from typing import cast, Dict, List, Union 4 | 5 | import looker_sdk 6 | from looker_sdk import models, error 7 | 8 | import sdk_exceptions 9 | 10 | sdk = looker_sdk.init31("../looker.ini") 11 | 12 | 13 | def main() -> None: 14 | """Given a look id, obtain the query behind it and run it with the desired 15 | filter values. 16 | 17 | https://docs.looker.com/reference/api-and-integration/api-reference/v3.1/query#implementation_notes_9 # noqa: B950 18 | shows an example of how filters are defined in the posted body. To set the 19 | same filter in this example, the script needs to be run as follows: 20 | 21 | $ python run_look_with_filters.py 5 category.name socks 22 | """ 23 | look_id = sys.argv[1] if len(sys.argv) > 1 else "" 24 | filter_args = iter(sys.argv[2:]) 25 | filters: Dict[str, str] = {} 26 | 27 | if not (look_id and len(sys.argv[2:]) > 0 and len(sys.argv[2:]) % 2 == 0): 28 | raise sdk_exceptions.ArgumentError( 29 | "Please provide: " 30 | " ..." 31 | ) 32 | 33 | for filter_name in filter_args: 34 | filters[filter_name] = next(filter_args) 35 | 36 | query = get_look_query(int(look_id)) 37 | results = run_query_with_filter(query, filters) 38 | 39 | print(f"Query results with filters={filters}:\n{results}", end="\n\n") 40 | 41 | 42 | def get_look_query(id: int) -> models.Query: 43 | """Returns the query associated with a given look id.""" 44 | try: 45 | look = sdk.look(id) 46 | except error.SDKError: 47 | raise sdk_exceptions.NotFoundError(f"Error getting Look {id}") 48 | else: 49 | query = look.query 50 | assert isinstance(query, models.Query) 51 | return query 52 | 53 | 54 | TJson = List[Dict[str, Union[str, int, float, bool, None]]] 55 | 56 | 57 | def run_query_with_filter(query: models.Query, filters: Dict[str, str]) -> TJson: 58 | """Runs the specified query with the specified filters.""" 59 | request = create_query_request(query, filters) 60 | try: 61 | json_ = sdk.run_inline_query("json", request, cache=False) 62 | except error.SDKError: 63 | raise sdk_exceptions.RunInlineQueryError("Error running query") 64 | else: 65 | json_resp = cast(TJson, json.loads(json_)) 66 | return json_resp 67 | 68 | 69 | def create_query_request(q: models.Query, filters: Dict[str, str]) -> models.WriteQuery: 70 | return models.WriteQuery( 71 | model=q.model, 72 | view=q.view, 73 | fields=q.fields, 74 | pivots=q.pivots, 75 | fill_fields=q.fill_fields, 76 | filters=filters, 77 | sorts=q.sorts, 78 | limit=q.limit, 79 | column_limit=q.column_limit, 80 | total=q.total, 81 | row_total=q.row_total, 82 | subtotals=q.subtotals, 83 | dynamic_fields=q.dynamic_fields, 84 | query_timezone=q.query_timezone, 85 | ) 86 | 87 | 88 | main() 89 | -------------------------------------------------------------------------------- /python/sdk_exceptions.py: -------------------------------------------------------------------------------- 1 | """Custom Exceptions 2 | """ 3 | 4 | 5 | class ArgumentError(Exception): 6 | pass 7 | 8 | 9 | class NotFoundError(Exception): 10 | pass 11 | 12 | 13 | class RenderTaskError(Exception): 14 | pass 15 | 16 | 17 | class RunInlineQueryError(Exception): 18 | pass 19 | -------------------------------------------------------------------------------- /python/soft_delete_dashboard.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from typing import Sequence 3 | 4 | import looker_sdk 5 | from looker_sdk import error, models 6 | 7 | import sdk_exceptions 8 | 9 | sdk = looker_sdk.init31("../looker.ini") 10 | 11 | 12 | def main(): 13 | """Given a dashboard title, get the ids of all dashboards with matching titles 14 | and move them to trash. 15 | 16 | $ python soft_delete_dashboard.py "An Unused Dashboard" 17 | """ 18 | 19 | dashboard_title = sys.argv[1] if len(sys.argv) > 1 else "" 20 | 21 | if not dashboard_title: 22 | raise sdk_exceptions.ArgumentError("Please provide: ") 23 | 24 | dashboards = get_dashboards(dashboard_title) 25 | delete_dashboards(dashboards) 26 | 27 | 28 | def get_dashboards(title: str) -> Sequence[models.Dashboard]: 29 | """Get dashboards with matching title""" 30 | lc_title = title.lower() 31 | results = sdk.search_dashboards(title=lc_title) 32 | if not results: 33 | raise sdk_exceptions.NotFoundError(f'dashboard "{title}" not found') 34 | assert isinstance(results, Sequence) 35 | return results 36 | 37 | 38 | def delete_dashboards(dashboards: Sequence[models.Dashboard]): 39 | """Soft delete dashboards""" 40 | for dashboard in dashboards: 41 | try: 42 | assert dashboard.id 43 | sdk.delete_dashboard(dashboard.id) 44 | except error.SDKError: 45 | print(f"Failed to delete dashboard with id {dashboard.id}.") 46 | else: 47 | print(f'"{dashboard.title}" (id {dashboard.id}) has been moved to trash.') 48 | 49 | 50 | main() 51 | -------------------------------------------------------------------------------- /python/test_connection.py: -------------------------------------------------------------------------------- 1 | from functools import reduce 2 | import sys 3 | from typing import cast, MutableSequence, Sequence 4 | 5 | import looker_sdk 6 | from looker_sdk import models 7 | 8 | import sdk_exceptions 9 | 10 | sdk = looker_sdk.init31("../looker.ini") 11 | 12 | 13 | def main(): 14 | """Given a connection, obtain its supported tests and run them. Example: 15 | 16 | $ python test_connection.py thelook 17 | """ 18 | connection_name = sys.argv[1] if len(sys.argv) > 1 else "" 19 | 20 | if not connection_name: 21 | raise sdk_exceptions.ArgumentError("Please provide a connection name") 22 | elif connection_name in ["looker", "looker__internal__analytics"]: 23 | raise sdk_exceptions.ArgumentError( 24 | f"Connection '{connection_name}' is internal and cannot be tested." 25 | ) 26 | 27 | connection = get_connections(connection_name) 28 | 29 | results = test_connection(connection) 30 | 31 | output_results(cast(str, connection.name), results) 32 | 33 | 34 | def get_connections(name: str) -> models.DBConnection: 35 | connection = sdk.connection(name, fields="name, dialect") 36 | return connection 37 | 38 | 39 | def test_connection( 40 | connection: models.DBConnection, 41 | ) -> Sequence[models.DBConnectionTestResult]: 42 | """Run supported tests against a given connection.""" 43 | assert connection.name 44 | assert connection.dialect and connection.dialect.connection_tests 45 | supported_tests: MutableSequence[str] = list(connection.dialect.connection_tests) 46 | test_results = sdk.test_connection( 47 | connection.name, models.DelimSequence(supported_tests) 48 | ) 49 | return test_results 50 | 51 | 52 | def output_results( 53 | connection_name: str, test_results: Sequence[models.DBConnectionTestResult] 54 | ): 55 | """Prints connection test results.""" 56 | errors = list(filter(lambda test: cast(str, test.status) == "error", test_results)) 57 | if errors: 58 | report = reduce( 59 | lambda failures, error: failures + f"\n - {error.message}", 60 | errors, 61 | f"{connection_name}:", 62 | ) 63 | else: 64 | report = f"All tests for connection '{connection_name}' were successful." 65 | print(report) 66 | 67 | 68 | main() 69 | -------------------------------------------------------------------------------- /r/README.md: -------------------------------------------------------------------------------- 1 | # R Examples for the Looker API 2 | 3 | You can find R language examples in this folder. 4 | -------------------------------------------------------------------------------- /ruby/README.md: -------------------------------------------------------------------------------- 1 | # Ruby Examples for the Looker API 2 | 3 | You can find Ruby language examples in this folder. 4 | 5 | 6 | Using Looker docs: https://docs.looker.com/reference/api-and-integration/api-reference 7 | Using official Looker Ruby SDK: https://github.com/looker/looker-sdk-ruby/ 8 | 9 | Use cases with Looker API: 10 | ___________ 11 | ### USERS 👤 12 | - Logout all users on the instance [[link]](https://github.com/looker-open-source/sdk-examples/blob/master/ruby/logout_all_users.rb) 13 | - Disable users in the instance [[link]](https://github.com/looker-open-source/sdk-examples/blob/master/ruby/disable_users.rb) 14 | - Get a list of all users and their auth credentials [[link]](https://github.com/looker-open-source/sdk-examples/blob/master/ruby/users_list_and_auth_types.rb) 15 | - Add all users to a group [[link]](https://github.com/looker-open-source/sdk-examples/blob/master/ruby/all_users_to_group.rb) 16 | 17 | 18 | ___________ 19 | ### CODE 💾 20 | - Get a list of all the Git branches in the projects [[link]](https://github.com/looker-open-source/sdk-examples/blob/master/ruby/all_git_branches.rb) 21 | - Validates the Looker **Projects** (LookML) [[link]](https://github.com/looker-open-source/sdk-examples/blob/master/ruby/validate_projects.rb) 22 | - Get a list of all files per projects [[link]](https://github.com/looker-open-source/sdk-examples/blob/master/ruby/list_files_per_project.rb) 23 | 24 | ___________ 25 | ### DELIVERY 📩 26 | - Render a Look as in PNG format [[link]](https://github.com/looker-open-source/sdk-examples/blob/master/ruby/render_look_png.rb) 27 | - Create a schedule to run once to Google Cloud Storage [[link]](https://github.com/looker-open-source/sdk-examples/blob/master/ruby/schedule_once_to_gcs.rb) 28 | - Stream results to S3 bucket [[link]](https://github.com/looker-open-source/sdk-examples/blob/master/ruby/stream_to_s3.rb) 29 | - Re-run the failed schedules to email [[link]](https://github.com/looker-open-source/sdk-examples/blob/master/ruby/rerun_failed_email_schedules.rb) 30 | - Download all dashboard tiles as standalone CSV files [[link]](https://github.com/looker-open-source/sdk-examples/blob/master/ruby/download_dashboard.rb#L12-L33) 31 | - Download all dashboard tiles as tabs in one Excel file [[link]](https://github.com/looker-open-source/sdk-examples/blob/master/ruby/download_dashboard.rb#L36-L65) 32 | - Test the integrations [[link]](https://github.com/looker-open-source/sdk-examples/blob/master/ruby/test_integrations.rb) 33 | 34 | ___________ 35 | ### CONTENT 📊 36 | - Template to update Look [[link]](https://github.com/looker-open-source/sdk-examples/blob/master/ruby/update_look.rb) 37 | - Automated testing for Look output between Development mode and Production mode [[link]](https://github.com/looker-open-source/sdk-examples/blob/master/ruby/master/dev_vs_prod.rb) 38 | - Validates the Looker **Content** [[link]](https://github.com/looker-open-source/sdk-examples/blob/master/ruby/validate_content.rb) 39 | - Delete old Looks [[link]](https://github.com/looker-open-source/sdk-examples/blob/master/ruby/delete_unused_content.rb) 40 | 41 | ___________ 42 | ### OTHER 🌐 43 | 44 | - Kill all running queries in the instance [[link]](https://github.com/looker-open-source/sdk-examples/blob/master/ruby/kill_all_running_queries.rb) 45 | - Test database connections [[link]](https://github.com/looker-open-source/sdk-examples/blob/master/ruby/test_all_connections.rb) 46 | - Create Looker Themes for your dashboards [[link]](https://github.com/looker-open-source/sdk-examples/blob/master/ruby/create_themes.rb) 47 | -------------------------------------------------------------------------------- /ruby/all_git_branches.rb: -------------------------------------------------------------------------------- 1 | require 'looker-sdk' 2 | 3 | # get API creds from environment variables 4 | sdk = LookerSDK::Client.new( 5 | :client_id => ENV['LOOKERSDK_CLIENT_ID'], 6 | :client_secret => ENV['LOOKERSDK_CLIENT_SECRET'], 7 | :api_endpoint => ENV['LOOKERSDK_BASE_URL'] 8 | ) 9 | 10 | all_projects = sdk.all_projects() 11 | 12 | # iterate thru all projects 13 | all_projects.each { |project| 14 | puts "Project name '#{project[:name]}' has the following branches:" 15 | all_branches = sdk.all_git_branches(project[:id], :fields => 'name, personal') 16 | 17 | # find all the branches 18 | all_branches.each { |branch| 19 | if branch[:personal] 20 | puts branch[:name].concat(" : Personal Branch") 21 | else 22 | branch[:name] == "master" ? (puts "#{branch[:name]} : Production Branch") : (puts "#{branch[:name]} : Shared Branch") 23 | 24 | end 25 | } 26 | puts "\n" 27 | } 28 | -------------------------------------------------------------------------------- /ruby/all_users_to_group.rb: -------------------------------------------------------------------------------- 1 | require 'looker-sdk' 2 | 3 | # get API creds from environment variables 4 | sdk = LookerSDK::Client.new( 5 | :client_id => ENV['LOOKERSDK_CLIENT_ID'], 6 | :client_secret => ENV['LOOKERSDK_CLIENT_SECRET'], 7 | :api_endpoint => ENV['LOOKERSDK_BASE_URL'] 8 | ) 9 | 10 | all_groups = sdk.all_groups(:fields => 'id, name') 11 | all_groups.each { |group| 12 | #change this value below to match exactly the group name you want 13 | if group[:name] == "Custom Fields Beta Users" 14 | puts "Group \"#{group[:name]}\" has ID #{group[:id]}" 15 | $group_id = group[:id] 16 | end 17 | } 18 | 19 | all_users = looker.all_users(:fields => 'id, email') 20 | all_users.each { |user| 21 | body = {"user_id": user[:id]} 22 | looker.add_group_user($group_id, body) 23 | # puts "User: #{user[:email].to_s.capitalize} with ID #{user[:id]} was added to the Group" 24 | } 25 | -------------------------------------------------------------------------------- /ruby/create_themes.rb: -------------------------------------------------------------------------------- 1 | require 'looker-sdk' 2 | 3 | # get API creds from environment variables 4 | sdk = LookerSDK::Client.new( 5 | :client_id => ENV['LOOKERSDK_CLIENT_ID'], 6 | :client_secret => ENV['LOOKERSDK_CLIENT_SECRET'], 7 | :api_endpoint => ENV['LOOKERSDK_BASE_URL'] 8 | ) 9 | ​ 10 | # NOTE: Custom themes needs to be enabled by Looker. 11 | # Unless custom themes are enabled, only the automatically generated default theme can be used. 12 | # Please contact your Account Manager or support@looker.com to update your license for this feature. 13 | 14 | # Example formatting for the theme details to be created 15 | # example_custom_theme = { 16 | # :name=> "my_custom_theme", 17 | # :settings=> { 18 | # :background_color=> "#d8e2f2", 19 | # :base_font_size=> "12px", 20 | # :color_collection_id=> "", 21 | # :font_color=> "#143f85", 22 | # :font_family=> "Arial", 23 | # :font_source=> "", 24 | # :info_button_color=> "#0087e1", 25 | # :primary_button_color=> "#64518a", 26 | # :show_filters_bar=> true, 27 | # :show_title=> true, 28 | # :text_tile_text_color=> "", 29 | # :tile_background_color=> "#efaa3b", 30 | # :tile_text_color=> "#3a4245", 31 | # :title_color=> "#7d0f48", 32 | # :warn_button_color=> "#980c11" 33 | # } 34 | # } 35 | ​ 36 | # these are general examples and not necessarily themes that follow the brand guidelines for these companies 37 | airbnb_like = {:name=>"airbnb", :settings=>{:background_color=>"#faf3f2", :base_font_size=>"12px", :color_collection_id=>"5f313589-67ce-44ba-b084-ec5107a7bb7e", :font_color=>"#143f85", :font_family=>"Arial", :font_source=>"", :info_button_color=>"#0087e1", :primary_button_color=>"#FF5A5F", :show_filters_bar=>true, :show_title=>true, :text_tile_text_color=>"", :tile_background_color=>"#faf3f2", :tile_text_color=>"#FF5A5F", :title_color=>"#FF5A5F", :warn_button_color=>"#980c11"}} 38 | gcp_like = {:name=>"gcp", :settings=>{:background_color=>"#ffffff", :base_font_size=>"12px", :color_collection_id=>"", :font_color=>"#4385f5", :font_family=>"Arial", :font_source=>"", :info_button_color=>"#fcbc05", :primary_button_color=>"#4385f5", :show_filters_bar=>true, :show_title=>true, :text_tile_text_color=>"", :tile_background_color=>"#ffffff", :tile_text_color=>"#4385f5", :title_color=>"#4385f5", :warn_button_color=>"#ea4136"}} 39 | aws_like = {:name=>"aws", :settings=>{:background_color=>"#000000", :base_font_size=>"12px", :color_collection_id=>"", :font_color=>"#ff9900", :font_family=>"Arial", :font_source=>"", :info_button_color=>"#FFAC31", :primary_button_color=>"#FFAC31", :show_filters_bar=>true, :show_title=>true, :text_tile_text_color=>"", :tile_background_color=>"#000000", :tile_text_color=>"#ff9900", :title_color=>"#ff9900", :warn_button_color=>"#ffc46d"}} 40 | microsoft_like = {:name=>"microsoft", :settings=>{:background_color=>"#ffffff", :base_font_size=>"12px", :color_collection_id=>"80e60a97-c02b-4a41-aa05-83522ee2144b", :font_color=>"#3a5998", :font_family=>"Arial", :font_source=>"", :info_button_color=>"#FFB900", :primary_button_color=>"#00A4EF", :show_filters_bar=>true, :show_title=>true, :text_tile_text_color=>"#7FBA00", :tile_background_color=>"#ffffff", :tile_text_color=>"#F25022", :title_color=>"#737373", :warn_button_color=>"#3a5998"}} 41 | facebook_like = {:name=>"facebook", :settings=>{:background_color=>"#f6f6f6", :base_font_size=>"12px", :color_collection_id=>"", :font_color=>"#3a5998", :font_family=>"Arial", :font_source=>"", :info_button_color=>"#3a5998", :primary_button_color=>"#3a5998", :show_filters_bar=>true, :show_title=>true, :text_tile_text_color=>"", :tile_background_color=>"#f6f6f6", :tile_text_color=>"#8b9dc3", :title_color=>"#4385f5", :warn_button_color=>"#3a5998"}} 42 | datadog_like = {:name=>"datadog", :settings=>{:background_color=>"#fdf0ff", :base_font_size=>"12px", :color_collection_id=>"5b121cce-cf79-457c-a52a-9162dc174766", :font_color=>"#774aa4", :font_family=>"Arial", :font_source=>"", :info_button_color=>"#774aa4", :primary_button_color=>"#774aa4", :show_filters_bar=>true, :show_title=>true, :text_tile_text_color=>"", :tile_background_color=>"#fdf0ff", :tile_text_color=>"#774aa4", :title_color=>"#774aa4", :warn_button_color=>"#774aa4"}} 43 | strava_like = {:name=>"strava", :settings=>{:background_color=>"#fff8f5", :base_font_size=>"12px", :color_collection_id=>"7c79334a-9912-4ca1-be6a-35756782ae09", :font_color=>"#FC4C02", :font_family=>"Arial", :font_source=>"", :info_button_color=>"#FC4C02", :primary_button_color=>"#FC4C02", :show_filters_bar=>true, :show_title=>true, :text_tile_text_color=>"", :tile_background_color=>"#fff8f5", :tile_text_color=>"#FC4C02", :title_color=>"#FC4C02", :warn_button_color=>"#FC4C02"}} 44 | sendgrid_like = {:name=>"sendgrid", :settings=>{:background_color=>"#f6f6f6", :base_font_size=>"12px", :color_collection_id=>"1bc1f1d8-7461-4bfd-8c3b-424b924287b5", :font_color=>"#1A82e2", :font_family=>"Arial", :font_source=>"", :info_button_color=>"#294661", :primary_button_color=>"#294661", :show_filters_bar=>true, :show_title=>true, :text_tile_text_color=>"", :tile_background_color=>"#f6f6f6", :tile_text_color=>"#1A82e2", :title_color=>"#1A82e2", :warn_button_color=>"#294661"}} 45 | ​ 46 | all_themes = [airbnb_like, gcp_like, aws_like, microsoft_like, facebook_like, datadog_like, strava_like, sendgrid_like] 47 | ​ 48 | # pull the list of themes we created above and create them all in our instance 49 | all_themes.each { | theme| 50 | sdk.create_theme(theme) 51 | } 52 | 53 | 54 | -------------------------------------------------------------------------------- /ruby/delete_unused_content.rb: -------------------------------------------------------------------------------- 1 | require 'looker-sdk' 2 | require 'time' 3 | 4 | # get API creds from environment variables 5 | sdk = LookerSDK::Client.new( 6 | :client_id => ENV['LOOKERSDK_CLIENT_ID'], 7 | :client_secret => ENV['LOOKERSDK_CLIENT_SECRET'], 8 | :api_endpoint => ENV['LOOKERSDK_BASE_URL'] 9 | ) 10 | 11 | # replace with the number of days you want 12 | $what_is_old = 360 13 | 14 | all_looks = sdk.all_looks(:fields => 'id, title, last_accessed_at, user') 15 | all_looks.each { | look | 16 | unless look[:last_accessed_at].nil? 17 | if (Time.now.utc - look[:last_accessed_at]) /60/60/24 > $what_is_old 18 | sdk.delete_look(look[:id]) 19 | puts "Look: #{look[:title].capitalize} was deleted!\tLast accessed #{((Time.now.utc-look[:last_accessed_at])/60/60/24 ).ceil} days ago" 20 | end 21 | end 22 | } 23 | -------------------------------------------------------------------------------- /ruby/dev_vs_prod.rb: -------------------------------------------------------------------------------- 1 | require 'looker-sdk' 2 | 3 | # get API creds from environment variables 4 | sdk = LookerSDK::Client.new( 5 | :client_id => ENV['LOOKERSDK_CLIENT_ID'], 6 | :client_secret => ENV['LOOKERSDK_CLIENT_SECRET'], 7 | :api_endpoint => ENV['LOOKERSDK_BASE_URL'] 8 | ) 9 | 10 | # Get the user input to test the Look (could be changed to read a file, or a list of looks to test) 11 | puts "Please enter the Look ID to use for the test?" 12 | look_to_get = gets.chomp 13 | 14 | 15 | # get look, and its attributes 16 | my_look = sdk.look(look_to_get) 17 | look_title = my_look["title"].to_s 18 | look_id = my_look["id"].to_s 19 | current_session = sdk.session["workspace_id"] 20 | 21 | puts "Testing the Look '#{look_title}', with ID '#{look_id}' for #{current_session} branch." 22 | 23 | # depending on the content used for testing, may need to use additional args for run_look(): 24 | # https://docs.looker.com/reference/api-and-integration/api-reference/look#run_look 25 | # rebuild_pdts: true ? 26 | 27 | prod_branch_results = sdk.run_look(look_to_get, 'csv', force_production: true) 28 | prod_branch_query = sdk.run_look(look_to_get, 'sql', force_production: true) 29 | 30 | # Changing to Dev branch to run the look there 31 | puts "Changing to dev mode." 32 | sdk.update_session("workspace_id": "dev") 33 | current_session = sdk.session["workspace_id"] 34 | 35 | puts "Testing the Look '#{look_title}', with ID '#{look_id}' for #{current_session} branch." 36 | dev_branch_results = sdk.run_look(look_to_get, 'csv', force_production: false) 37 | dev_branch_query = sdk.run_look(look_to_get, 'sql', force_production: false) 38 | 39 | 40 | if prod_branch_results == dev_branch_results 41 | puts "Success! Production data matches your dev mode data." 42 | else 43 | # let's get some details on values and queries 44 | File.open("production_output.csv", 'w') { |file| file.write(prod_branch_results) } 45 | File.open("production_query.txt", 'w') { |file| file.write(prod_branch_query) } 46 | 47 | File.open("development_output.csv", 'w') { |file| file.write(dev_branch_results) } 48 | File.open("development_query.txt", 'w') { |file| file.write(dev_branch_query) } 49 | puts "The outputs are not identical, please check out the output files for details" 50 | end 51 | -------------------------------------------------------------------------------- /ruby/disable_users.rb: -------------------------------------------------------------------------------- 1 | require 'looker-sdk' 2 | 3 | # get API creds from environment variables 4 | sdk = LookerSDK::Client.new( 5 | :client_id => ENV['LOOKERSDK_CLIENT_ID'], 6 | :client_secret => ENV['LOOKERSDK_CLIENT_SECRET'], 7 | :api_endpoint => ENV['LOOKERSDK_BASE_URL'] 8 | ) 9 | 10 | # list the users to disable in the instance 11 | users_to_disable = [264, 214, 260] 12 | body = {"is_disabled": true} 13 | 14 | 15 | users_to_disable.each { |item| 16 | sdk.update_user(item, body) 17 | puts "User '#{sdk.user(item)[:first_name]} #{sdk.user(item)[:last_name]}' was disabled (id: #{item})" 18 | } 19 | -------------------------------------------------------------------------------- /ruby/download_dashboard.rb: -------------------------------------------------------------------------------- 1 | require 'looker-sdk' 2 | require 'xlsxtream' # https://github.com/felixbuenemann/xlsxtream 3 | 4 | # get API creds from environment variables 5 | sdk = LookerSDK::Client.new( 6 | :client_id => ENV['LOOKERSDK_CLIENT_ID'], 7 | :client_secret => ENV['LOOKERSDK_CLIENT_SECRET'], 8 | :api_endpoint => ENV['LOOKERSDK_BASE_URL'] 9 | ) 10 | 11 | 12 | def one_csv_per_tile(dashboard_to_export, looker) 13 | #function to download the results for each dashboard query as its own csv file 14 | begin 15 | dash_info = looker.dashboard(dashboard_to_export) 16 | rescue StandardError => msg 17 | puts "#{msg}" 18 | puts "⚠️\tDashboard ID Not Found" 19 | end 20 | 21 | dash_info[:dashboard_elements].each { | dashboardelem | 22 | 23 | unless dashboardelem[:resultmaker].nil? && dashboardelem[:type] != "vis" 24 | 25 | query_id = dashboardelem[:result_maker][:query_id] 26 | $cleaned_tile_title = dashboardelem[:title].gsub(/[^0-9a-z ]/i, '').gsub(/[ ]/i, '_') 27 | results = looker.run_query(query_id, "csv") 28 | 29 | puts "Processing results for tile: #{$cleaned_tile_title}..." 30 | File.open("#{$cleaned_tile_title}.csv", 'w') { |file| file.write(results) } 31 | end 32 | } 33 | end 34 | 35 | 36 | def one_excel_for_dash(dashboard_to_export, looker) 37 | #function to download the results for each dashboard query as a tab in one Excel file 38 | begin 39 | dash_info = looker.dashboard(dashboard_to_export) 40 | rescue StandardError => msg 41 | puts "#{msg}" 42 | puts "⚠️\tDashboard ID Not Found" 43 | end 44 | $cleaned_dash_title = dash_info[:title].gsub(/[^0-9a-z ]/i, '').gsub(/[ ]/i, '_') 45 | 46 | Xlsxtream::Workbook.open("#{$cleaned_dash_title}.xlsx") do | xlsx | 47 | 48 | dash_info[:dashboard_elements].each { | dashboardelem | 49 | 50 | unless dashboardelem[:resultmaker].nil? && dashboardelem[:type] != "vis" 51 | 52 | query_id = dashboardelem[:result_maker][:query_id] 53 | $cleaned_tile_title = dashboardelem[:title].gsub(/[^0-9a-z ]/i, '').gsub(/[ ]/i, '_') 54 | results = looker.run_query(query_id, "csv") 55 | 56 | puts "Processing results for tile: #{$cleaned_tile_title}..." 57 | xlsx.write_worksheet "#{$cleaned_tile_title}" do | sheet | 58 | results.each_line do | line | 59 | sheet << line.split(',') 60 | end 61 | end 62 | end 63 | } 64 | end 65 | end 66 | 67 | # change the number below to match the id of the dashboard to download 68 | dashboard_to_export = 23 69 | 70 | # uncomment the function you need 71 | # one_csv_per_tile(dashboard_to_export, sdk) 72 | # one_excel_for_dash(dashboard_to_export, sdk) 73 | -------------------------------------------------------------------------------- /ruby/kill_all_running_queries.rb: -------------------------------------------------------------------------------- 1 | require 'looker-sdk' 2 | 3 | # get API creds from environment variables 4 | sdk = LookerSDK::Client.new( 5 | :client_id => ENV['LOOKERSDK_CLIENT_ID'], 6 | :client_secret => ENV['LOOKERSDK_CLIENT_SECRET'], 7 | :api_endpoint => ENV['LOOKERSDK_BASE_URL'] 8 | ) 9 | 10 | all_running_queries = sdk.all_running_queries() 11 | all_running_queries.each { |item| 12 | puts "Killing query: #{item[:query_task_id]} from #{item[:source].to_s.capitalize}" 13 | sdk.kill_query(item[:query_task_id]) 14 | } 15 | -------------------------------------------------------------------------------- /ruby/list_files_per_project.rb: -------------------------------------------------------------------------------- 1 | require 'looker-sdk' 2 | 3 | # get API creds from environment variables 4 | sdk = LookerSDK::Client.new( 5 | :client_id => ENV['LOOKERSDK_CLIENT_ID'], 6 | :client_secret => ENV['LOOKERSDK_CLIENT_SECRET'], 7 | :api_endpoint => ENV['LOOKERSDK_BASE_URL'] 8 | ) 9 | 10 | overall_file_types = Array.new 11 | all_projects = sdk.all_projects(:fields => 'id') 12 | 13 | # iterate through all projects 14 | all_projects.each { |project| 15 | 16 | 17 | file_types = Array.new 18 | puts "\n" 19 | puts "*"*60 20 | puts "Project: #{project[:id]}" 21 | 22 | project_files_list = sdk.all_project_files(project[:id], :fields => 'id, type, path') 23 | project_files_list.each { |file| 24 | 25 | # add the type to the array to count later on 26 | file_types.push(file[:type]) 27 | overall_file_types.push(file[:type]) 28 | 29 | } 30 | 31 | # get a count of each individual value in the array 32 | file_types_counted = file_types.group_by{|e| e}.map{|k, v| [k, v.length]}.to_h 33 | puts file_types_counted.map{ |k,v| "#{k} => #{v}" }.sort.reverse 34 | 35 | } 36 | 37 | puts "\n" 38 | puts "*"*60 39 | puts "SUMMARY: " 40 | all_types_counted = overall_file_types.group_by{|e| e}.map{|k, v| [k, v.length]}.to_h 41 | puts all_types_counted.map{ |k,v| "#{k} => #{v}" }.sort.reverse 42 | puts "*"*60 43 | -------------------------------------------------------------------------------- /ruby/list_users_and_auth_type.rb: -------------------------------------------------------------------------------- 1 | require 'looker-sdk' 2 | 3 | # get API creds from environment variables 4 | sdk = LookerSDK::Client.new( 5 | :client_id => ENV['LOOKERSDK_CLIENT_ID'], 6 | :client_secret => ENV['LOOKERSDK_CLIENT_SECRET'], 7 | :api_endpoint => ENV['LOOKERSDK_BASE_URL'] 8 | ) 9 | 10 | response = sdk.all_users() 11 | 12 | response.each { |user| 13 | user.each { | creds | 14 | unless creds[1].inspect.nil? 15 | if creds[0].inspect.include? "credentials_" 16 | if creds[1].class.to_s == 'Sawyer::Resource' 17 | puts "Auth type: #{creds[0]} for user #{creds[1][:email].inspect} with User ID: #{user[:id]}" unless creds[1][:email].nil? 18 | end 19 | end 20 | end 21 | } 22 | } 23 | 24 | puts "\nThere are #{response.length} users in the instance." 25 | -------------------------------------------------------------------------------- /ruby/logout_all_users.rb: -------------------------------------------------------------------------------- 1 | require 'looker-sdk' 2 | 3 | # get API creds from environment variables 4 | sdk = LookerSDK::Client.new( 5 | :client_id => ENV['LOOKERSDK_CLIENT_ID'], 6 | :client_secret => ENV['LOOKERSDK_CLIENT_SECRET'], 7 | :api_endpoint => ENV['LOOKERSDK_BASE_URL'] 8 | ) 9 | 10 | all_users = sdk.all_users() 11 | count_users = 0 12 | all_users.each { |item| 13 | if not item[:sessions][0].nil? 14 | sdk.delete_user_session(item[:id], item[:sessions][0][:id]) 15 | puts "Logged out user: #{item[:first_name].to_s.capitalize} #{item[:last_name].to_s.capitalize}" 16 | count_users += 1 17 | end 18 | } 19 | puts "\nScript logged out #{count_users} users" 20 | -------------------------------------------------------------------------------- /ruby/render_look_png.rb: -------------------------------------------------------------------------------- 1 | require 'looker-sdk' 2 | 3 | # get API creds from environment variables 4 | sdk = LookerSDK::Client.new( 5 | :client_id => ENV['LOOKERSDK_CLIENT_ID'], 6 | :client_secret => ENV['LOOKERSDK_CLIENT_SECRET'], 7 | :api_endpoint => ENV['LOOKERSDK_BASE_URL'] 8 | ) 9 | 10 | look_to_render = 44 11 | 12 | png = sdk.create_look_render_task(look_to_render, "png", {}, {:query => {:width => 1000, :height => 600}}) 13 | 14 | id = png[:id] 15 | 16 | until sdk.render_task(id)[:status] == 'success' do 17 | end 18 | 19 | results = sdk.render_task_results(id) 20 | 21 | File.open('look.png', 'w') { |file| file.write(results) } 22 | -------------------------------------------------------------------------------- /ruby/rerun_failed_email_schedules.rb: -------------------------------------------------------------------------------- 1 | require 'looker-sdk' 2 | require 'date' 3 | require 'json' 4 | 5 | # get API creds from environment variables 6 | sdk = LookerSDK::Client.new( 7 | :client_id => ENV['LOOKERSDK_CLIENT_ID'], 8 | :client_secret => ENV['LOOKERSDK_CLIENT_SECRET'], 9 | :api_endpoint => ENV['LOOKERSDK_BASE_URL'] 10 | ) 11 | 12 | # retrieve the content for the look listing failed schedule job (in this case look_id = 120) 13 | # look is generated from the i__looker model so user creating it needs to be an ADMIN user 14 | # https://COMPANY.looker.com/explore/i__looker/scheduled_plan?fields=scheduled_job.status,scheduled_plan.id,scheduled_job.created_date,scheduled_plan_destination.format,scheduled_plan_destination.type,scheduled_job.status_detail&f[scheduled_plan.run_once]=no&f[scheduled_job.status]=failure&f[scheduled_plan_destination.type]=email&f[scheduled_job.created_date]=this+week&sorts=scheduled_job.created_date,scheduled_plan.id+desc&limit=500 15 | failed_schedules = sdk.run_look(120, "json") 16 | 17 | 18 | failed_schedules.each { |schedule| 19 | puts "*"*60 20 | puts "The schedule #{schedule["scheduled_plan.id"]} failed:" 21 | puts "\tDate:\t #{schedule["scheduled_job.created_date"]}" 22 | puts "\tError:\t #{schedule["scheduled_job.status_detail"]}" 23 | puts "\tRecipients: #{schedule["scheduled_plan.destination_addresses"]}" 24 | puts "\tFormat:\t #{schedule["scheduled_plan_destination.format"]}\n" 25 | 26 | 27 | puts "\nGetting details of the failed schedule" 28 | old_schedule = sdk.scheduled_plan(schedule["scheduled_plan.id"]) 29 | 30 | print "\nRe-scheduling the content now to the same email" 31 | rerun_date = " - reran from API on ".concat(Date.today.to_s) 32 | 33 | new_schedule = old_schedule 34 | new_schedule[:title] = new_schedule[:title].concat(rerun_date) 35 | new_schedule[:name] = new_schedule[:name].concat(rerun_date) 36 | new_schedule[:id] = nil 37 | # so the schedule is associated with the API user for the script 38 | new_schedule[:user] = nil 39 | 40 | run_it = sdk.scheduled_plan_run_once(new_schedule.to_h.to_json) 41 | puts "\t✅ - Sent" 42 | } 43 | -------------------------------------------------------------------------------- /ruby/schedule_once_to_gcs.rb: -------------------------------------------------------------------------------- 1 | require 'looker-sdk' 2 | 3 | # This function requires prior settings on the Looker instance for the Action Hub and Google Cloud Storage 4 | # https://docs.looker.com/admin-options/platform/actions#how_to_enable_an_action 5 | 6 | def send_csv_to_gcs 7 | sdk = LookerSDK::Client.new( 8 | :client_id => ENV['LOOKERSDK_CLIENT_ID'], 9 | :client_secret => ENV['LOOKERSDK_CLIENT_SECRET'], 10 | :api_endpoint => ENV['LOOKERSDK_BASE_URL'] 11 | ) 12 | 13 | response = sdk.look(487) # change this id with the id of the Look you want to send to GCS 14 | $query_id_for_look = response[:query_id].inspect 15 | $filename = "historybackup_" + Time.now.strftime("%Y%m%d").to_s 16 | 17 | schedule_hash = { 18 | :name=>$filename, 19 | :query_id => $query_id_for_look, 20 | :run_once => true, 21 | :require_results => false, 22 | :require_no_results => false, 23 | :require_change => false, 24 | :send_all_results => false, 25 | :scheduled_plan_destination => [{ 26 | :format => "csv", 27 | :apply_formatting => false, 28 | :address => "", 29 | :type => "looker-integration://1::google_cloud_storage", 30 | :parameters => "{\"bucket\":\"backupforlooker\",\"filename\":\"#{$filename}\"}" 31 | }] 32 | } 33 | # replace 'backupforlooker' with the name of your bucket 34 | 35 | #create and run the schedule 36 | schedule = sdk.scheduled_plan_run_once(schedule_hash).inspect 37 | 38 | puts "Schedule to Google Cloud Storage processed." 39 | end 40 | -------------------------------------------------------------------------------- /ruby/stream_to_s3.rb: -------------------------------------------------------------------------------- 1 | require 'looker-sdk' 2 | require 'aws-sdk-s3' 3 | require 'securerandom' 4 | 5 | sdk = LookerSDK::Client.new( 6 | :client_id => ENV['LOOKERSDK_CLIENT_ID'], 7 | :client_secret => ENV['LOOKERSDK_CLIENT_SECRET'], 8 | :api_endpoint => ENV['LOOKERSDK_BASE_URL'] 9 | ) 10 | 11 | #define all the variables for Looker and S3 credentials 12 | $query_id_for_look = 14964 # update the query id for the query of the Look to use 13 | $access_key_id = "REPLACE_WITH_YOUR_IAM_KEY_ID" 14 | $secret_access_key = "REPLACE_WITH_YOUR_IAM_SECRET_ACCESS" 15 | $unique_name = SecureRandom.hex 16 | 17 | schedule_hash = { 18 | :name=>$unique_name, 19 | :query_id => $query_id_for_look, 20 | :run_once => true, 21 | :send_all_results => true, 22 | :scheduled_plan_destination=>[{ 23 | :format=>"csv", 24 | :apply_formatting=>false, 25 | :address=>"s3://looker-api-schedules", 26 | :type=>"s3", 27 | :parameters=>"{\"region\":\"us-east-1\",\"access_key_id\":\"#{access_key_id}\"", 28 | :secret_parameters=>"{\"secret_access_key\":\"#{secret_access_key}\"}" 29 | }] 30 | } 31 | 32 | #create the schedule 33 | schedule = sdk.scheduled_plan_run_once(schedule_hash).inspect 34 | 35 | #wait to run the next code to make sure that schedule has been created 36 | sleep(3) 37 | 38 | #get all the running queries and then loop through them to find the task assocaited with our schedule 39 | running_queries = sdk.all_running_queries() 40 | 41 | running_queries.each do |query| 42 | if query[:query_id] == $query_id_for_look 43 | $query_task = query[:query_task_id] 44 | end 45 | end 46 | 47 | 48 | $status = sdk.query_task($query_task) 49 | 50 | #poll the query task until it is done before we go get it from s3 51 | while true 52 | if $status[:status] == "complete" 53 | break 54 | end 55 | $status = sdk.query_task($query_task) 56 | end 57 | 58 | 59 | s3 = Aws::S3::Resource.new(region: 'us-east-1') 60 | bucket = s3.bucket('bucket_name') 61 | 62 | 63 | #loop through the bucket and find the file that we created, we check to see if the unique name is in the filename 64 | bucket.objects.limit(1000).each do |item| 65 | if item.key.include? $unique_name 66 | $file_name = item.key 67 | end 68 | end 69 | 70 | 71 | # Create the object to retrieve 72 | obj = s3.bucket('looker-api-schedules').object($file_name) 73 | 74 | # Get the item's content and save it to a file 75 | obj.get(response_target: './output.csv') 76 | -------------------------------------------------------------------------------- /ruby/test_all_connections.rb: -------------------------------------------------------------------------------- 1 | require 'looker-sdk' 2 | 3 | # get API creds from environment variables 4 | sdk = LookerSDK::Client.new( 5 | :client_id => ENV['LOOKERSDK_CLIENT_ID'], 6 | :client_secret => ENV['LOOKERSDK_CLIENT_SECRET'], 7 | :api_endpoint => ENV['LOOKERSDK_BASE_URL'] 8 | ) 9 | 10 | def test_all_connections(looker) 11 | all_connections = looker.all_connections(:fields => 'name, port, host, dialect(connection_tests, label)') 12 | test_results = [] 13 | 14 | all_connections.each { | connection | 15 | puts "Processing connection tests for \"#{connection[:name]}\"" 16 | begin 17 | tests_to_run = connection[:dialect][:connection_tests].join(',') 18 | 19 | testing = looker.test_connection(connection[:name], {}, tests: tests_to_run) 20 | testing.each { | test | 21 | if test[:status] != "success" 22 | test_results << "Connection \"#{connection[:name]}\" (#{connection[:dialect][:label]}) has #{test[:status]}!" 23 | end 24 | } 25 | rescue 26 | test_results << "Uncaught error in testing connection \"#{connection[:name]}\"" 27 | end 28 | } 29 | puts "\n**********" 30 | return test_results 31 | end 32 | 33 | puts test_all_connections(sdk) 34 | -------------------------------------------------------------------------------- /ruby/test_integrations.rb: -------------------------------------------------------------------------------- 1 | require 'looker-sdk' 2 | 3 | # get API creds from environment variables 4 | sdk = LookerSDK::Client.new( 5 | :client_id => ENV['LOOKERSDK_CLIENT_ID'], 6 | :client_secret => ENV['LOOKERSDK_CLIENT_SECRET'], 7 | :api_endpoint => ENV['LOOKERSDK_BASE_URL'] 8 | ) 9 | 10 | all_integrations = sdk.all_integrations(:fields => 'id, label, enabled') 11 | 12 | all_integrations.each { | integration | 13 | begin 14 | testing = sdk.test_integration(integration[:id]) 15 | puts testing[:success] ? "Test OK\t\t#{integration[:label]}" : "Not Enabled\t#{integration[:label]}" 16 | rescue 17 | not_enabled = "Error with test for integration #{integration[:label]}" 18 | puts !integration[:enabled] ? not_enabled : "Unknown error\t#{integration[:label]}" 19 | end 20 | } 21 | -------------------------------------------------------------------------------- /ruby/update_look.rb: -------------------------------------------------------------------------------- 1 | require 'looker-sdk' 2 | 3 | # get API creds from environment variables 4 | sdk = LookerSDK::Client.new( 5 | :client_id => ENV['LOOKERSDK_CLIENT_ID'], 6 | :client_secret => ENV['LOOKERSDK_CLIENT_SECRET'], 7 | :api_endpoint => ENV['LOOKERSDK_BASE_URL'] 8 | ) 9 | 10 | # get look, here look id 32 11 | my_look = sdk.look(32) 12 | 13 | # get query id for look 14 | my_query = sdk.query(my_look.query_id).to_attrs 15 | 16 | # set new filters, here update the value for order_id 17 | my_query[:filters] = {:"order_items.order_id" => "<567"} 18 | 19 | # remove the client id! 20 | my_query[:client_id] = {} 21 | 22 | # create a new query 23 | my_new_query = sdk.create_query(my_query) 24 | puts "New Query ID: " + my_new_query[:id].to_s 25 | 26 | # update look with new query 27 | my_look = sdk.update_look(32, :query_id => my_new_query[:id]) 28 | puts "Updated Look Query ID: " + my_look[:query_id].to_s 29 | 30 | 31 | if my_new_query[:id] == my_look[:query_id] 32 | puts "Success!" 33 | else 34 | puts "Fail again, fail better" 35 | end 36 | -------------------------------------------------------------------------------- /ruby/users_list_and_auth_types.rb: -------------------------------------------------------------------------------- 1 | require 'looker-sdk' 2 | 3 | # get API creds from environment variables 4 | sdk = LookerSDK::Client.new( 5 | :client_id => ENV['LOOKERSDK_CLIENT_ID'], 6 | :client_secret => ENV['LOOKERSDK_CLIENT_SECRET'], 7 | :api_endpoint => ENV['LOOKERSDK_BASE_URL'] 8 | ) 9 | 10 | # get info for all users for these fields 11 | details_for_users = sdk.all_users(:fields => 'id, first_name, last_name, credentials_email, credentials_totp, 12 | credentials_ldap, credentials_google, credentials_saml, credentials_oidc, 13 | credentials_api3, crendentials_embed, credentials_looker_openid, sessions') 14 | 15 | # go through the users to get out output 16 | details_for_users.each { |user| 17 | user.each { | creds | 18 | unless creds[1].inspect.nil? 19 | if creds[1].class.to_s == 'Sawyer::Resource' 20 | 21 | user_name = user[:first_name].to_s + " " + user[:last_name].to_s 22 | puts "User #{user_name} with User ID: #{user[:id]}" unless creds[1][:email].nil? 23 | puts "Auth type: #{creds[0]} for email #{creds[1][:email].inspect}\n" unless creds[1][:email].nil? 24 | 25 | end 26 | end 27 | } 28 | } 29 | 30 | puts "\nThere are #{details_for_users.length} users in the instance." 31 | -------------------------------------------------------------------------------- /ruby/validate_content.rb: -------------------------------------------------------------------------------- 1 | require 'looker-sdk' 2 | 3 | # get API creds from environment variables 4 | sdk = LookerSDK::Client.new( 5 | :client_id => ENV['LOOKERSDK_CLIENT_ID'], 6 | :client_secret => ENV['LOOKERSDK_CLIENT_SECRET'], 7 | :api_endpoint => ENV['LOOKERSDK_BASE_URL'] 8 | ) 9 | 10 | # adding metrics for our validation summary 11 | error_messages = Array.new() 12 | model_with_errors = Array.new() 13 | look_with_errors, dashboard_with_errors = 0, 0 14 | 15 | content_validated = sdk.content_validation() 16 | 17 | # iterate through the response to print it individually and calculate summary 18 | content_validated[:content_with_errors].each { |content| 19 | content[:errors].each { |error| 20 | if content[:look].nil? 21 | puts "\nContent from Dashboard with ID: #{content[:dashboard][:id]}" 22 | dashboard_with_errors += 1 23 | else 24 | puts "Content from Look with ID: #{content[:look][:id]}" 25 | look_with_errors += 1 26 | end 27 | puts "Error Message:\t #{error[:message].inspect}" 28 | puts "in Model:\t #{error[:model_name].inspect}" 29 | error_messages.push(error[:message].inspect) 30 | model_with_errors.push(error[:model_name].inspect) 31 | } 32 | 33 | } 34 | puts "\n" 35 | puts "*"*60 36 | puts "SUMMARY:" 37 | 38 | # list to dictionary to keep unique values and count occurences 39 | dict_errors = error_messages.group_by{|e| e}.map{|k, v| [k, v.length]}.to_h 40 | dict_models = model_with_errors.group_by{|e| e}.map{|k, v| [k, v.length]}.to_h 41 | 42 | 43 | puts "\nErrors encountered:" 44 | puts dict_errors.map{ |k,v| "#{k} => #{v}" }.sort.reverse 45 | puts "\nModels with Errors:" 46 | puts dict_models.map{ |k,v| "#{k} => #{v}" }.sort.reverse 47 | 48 | puts "\n\n" 49 | puts "The Content Validator found #{error_messages.size} errors in total for: " 50 | puts "\t - #{look_with_errors} Looks" 51 | puts "\t - #{dashboard_with_errors} Dashboards" 52 | puts "\t - #{model_with_errors.uniq!.size} Models" 53 | puts "*"*60 54 | -------------------------------------------------------------------------------- /ruby/validate_projects.rb: -------------------------------------------------------------------------------- 1 | require 'looker-sdk' 2 | 3 | # get API creds from environment variables 4 | sdk = LookerSDK::Client.new( 5 | :client_id => ENV['LOOKERSDK_CLIENT_ID'], 6 | :client_secret => ENV['LOOKERSDK_CLIENT_SECRET'], 7 | :api_endpoint => ENV['LOOKERSDK_BASE_URL'] 8 | ) 9 | 10 | # adding metrics for our validation summary 11 | validating_count = 0 12 | validation_timing = Array.new 13 | 14 | all_projects = sdk.all_projects(:fields => 'id') 15 | 16 | # iterate through all projects 17 | all_projects.each { |project| 18 | puts "\n" 19 | puts "Validating project #{project[:id]} :" 20 | 21 | # run the validator on the project to get the errors and time taken 22 | all_validation_output = sdk.validate_project(project[:id], :fields => 'errors,computation_time') 23 | all_validation_output[:errors].each { |error| 24 | puts error.inspect 25 | } 26 | 27 | validating_count += 1 28 | validation_timing.push(all_validation_output[:computation_time]) 29 | 30 | puts "Total computation time for #{all_validation_output[:errors].size} errors was #{all_validation_output[:computation_time]}" 31 | } 32 | 33 | # summary from validation: 34 | avg_runtime_validation = validation_timing.inject{ |sum, el| sum + el }.to_f / validation_timing.size 35 | 36 | puts "\nValidation performed #{validating_count} times" 37 | puts "Average runtime for the validation was #{avg_runtime_validation}" 38 | -------------------------------------------------------------------------------- /swift/README.md: -------------------------------------------------------------------------------- 1 | # Swift Examples for the Looker API 2 | 3 | You can find Swift language examples in this folder. 4 | 5 | The examples are based on Swift 5.1 and above. 6 | -------------------------------------------------------------------------------- /swift/sample-swift-sdk/README.md: -------------------------------------------------------------------------------- 1 | ## Sample iOS Swift SDK Example 2 | 3 | ### Setting the Looker SDK configuration 4 | 5 | There are a couple different ways to configure the SDK for a Looker server. 6 | This example sets them as environment variables. 7 | To set the environment vars go to `Product`>`Scheme`>`Edit Scheme...` 8 | and add 9 | 10 | - `LOOKERSDK_BASE_URL` for the Looker API server URL, like `https://foo.looker.com:19999` 11 | - `LOOKER_API_VERSION` as `3.1` 12 | - `LOOKERSDK_CLIENT_ID` for the API 3 Client ID 13 | - `LOOKERSDK_CLIENT_SECRET` for the API 3 Client Secret 14 | -------------------------------------------------------------------------------- /swift/sample-swift-sdk/sample-swift-sdk.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /swift/sample-swift-sdk/sample-swift-sdk.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /swift/sample-swift-sdk/sample-swift-sdk.xcodeproj/xcuserdata/Macallan.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 9 | 21 | 22 | 23 | 25 | 37 | 38 | 39 | 41 | 53 | 54 | 55 | 57 | 69 | 70 | 71 | 73 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /swift/sample-swift-sdk/sample-swift-sdk.xcodeproj/xcuserdata/Macallan.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | sample-swift-sdk.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | CAEB97772370B2C20050E540 16 | 17 | primary 18 | 19 | 20 | CAEB978D2370B2C60050E540 21 | 22 | primary 23 | 24 | 25 | CAEB97982370B2C60050E540 26 | 27 | primary 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /swift/sample-swift-sdk/sample-swift-sdk/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // sample-swift-sdk 4 | // 5 | 6 | import UIKit 7 | 8 | @UIApplicationMain 9 | 10 | class AppDelegate: UIResponder, UIApplicationDelegate { 11 | 12 | var sdk: LookerSDK! 13 | 14 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 15 | 16 | let config = ApiConfig() 17 | // In the scheme make sure to set 18 | // LOOKERSDK_BASE_URL to the API server url 19 | // LOOKER_SDK_API_VERSION to 3.1 20 | // LOOKERSDK_CLIENT_ID to the API3 client id 21 | // LOOKERSDK_CLIENT_SECRET to the 22 | // as environment variables 23 | let xp = BaseTransport(config) 24 | let auth = AuthSession(config, xp) 25 | 26 | sdk = LookerSDK(auth) 27 | 28 | return true 29 | } 30 | 31 | // MARK: UISceneSession Lifecycle 32 | 33 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 34 | // Called when a new scene session is being created. 35 | // Use this method to select a configuration to create the new scene with. 36 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 37 | } 38 | 39 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 40 | // Called when the user discards a scene session. 41 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 42 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 43 | } 44 | 45 | 46 | } 47 | 48 | -------------------------------------------------------------------------------- /swift/sample-swift-sdk/sample-swift-sdk/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /swift/sample-swift-sdk/sample-swift-sdk/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /swift/sample-swift-sdk/sample-swift-sdk/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /swift/sample-swift-sdk/sample-swift-sdk/Dashboards.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dashboards.swift 3 | // sample-swift-sdk 4 | // 5 | 6 | import Foundation 7 | import UIKit 8 | 9 | struct Dashboards { 10 | var all:[DashboardModel] = [] 11 | 12 | // Create struct that inherits identifiable for view 13 | struct DashboardModel: Identifiable { 14 | var id: Int64 15 | var title: String 16 | 17 | init(id: Int64, title: String) { 18 | self.id = id 19 | self.title = title 20 | } 21 | } 22 | 23 | init() { 24 | let appDelegate = UIApplication.shared.delegate as! AppDelegate 25 | let sdk = appDelegate.sdk! 26 | 27 | // Grab the title and the content_metadata_id from the results 28 | for dashboard in sdk.ok(sdk.all_dashboards(fields: "title,content_metadata_id")) { 29 | all.append(DashboardModel(id: dashboard.content_metadata_id ?? 0, title: dashboard.title ?? "")) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /swift/sample-swift-sdk/sample-swift-sdk/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | 37 | 38 | 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UISupportedInterfaceOrientations~ipad 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationPortraitUpsideDown 56 | UIInterfaceOrientationLandscapeLeft 57 | UIInterfaceOrientationLandscapeRight 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /swift/sample-swift-sdk/sample-swift-sdk/LookerSwiftSDK/apiConfig.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2019 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 | // INI parser from https://gist.github.com/jetmind/f776c0d223e4ac6aec1ff9389e874553 26 | 27 | import Foundation 28 | 29 | typealias SectionConfig = [String: String] 30 | typealias Config = [String: SectionConfig] 31 | 32 | func trim(_ s: String) -> String { 33 | let whitespaces = CharacterSet(charactersIn: " \n\r\t") 34 | return s.trimmingCharacters(in: whitespaces) 35 | } 36 | 37 | func stripComment(_ line: String) -> String { 38 | let parts = line.split( 39 | separator: "#", 40 | maxSplits: 1, 41 | omittingEmptySubsequences: false) 42 | if parts.count > 0 { 43 | return String(parts[0]) 44 | } 45 | return "" 46 | } 47 | 48 | func parseSectionHeader(_ line: String) -> String { 49 | let from = line.index(after: line.startIndex) 50 | let to = line.index(before: line.endIndex) 51 | let range = from.. (String, String)? { 56 | let parts = stripComment(line).split(separator: "=", maxSplits: 1) 57 | if parts.count == 2 { 58 | let k = trim(String(parts[0])) 59 | let v = trim(String(parts[1])) 60 | return (k, v) 61 | } 62 | return nil 63 | } 64 | 65 | func parseConfig(_ filename : String) -> Config { 66 | let f = try! String(contentsOfFile: filename) 67 | var config = Config() 68 | var currentSectionName = "Looker" 69 | for line in f.components(separatedBy: "\n") { 70 | let line = trim(line) 71 | if line.hasPrefix("[") && line.hasSuffix("]") { 72 | currentSectionName = parseSectionHeader(line) 73 | } else if let (k, v) = parseLine(line) { 74 | var section = config[currentSectionName] ?? [:] 75 | section[k] = v 76 | config[currentSectionName] = section 77 | } 78 | } 79 | return config 80 | } 81 | 82 | struct ApiConfig : IApiSettings { 83 | func readConfig(_ section: String? = nil) -> IApiSection { 84 | return [:] 85 | // let config = parseConfig(self.fileName) 86 | // return config[section ?? self.section] ?? [:] 87 | } 88 | 89 | func isConfigured() -> Bool { 90 | return (base_url != "" && api_version != "") 91 | } 92 | 93 | var base_url: String? 94 | 95 | var api_version: String? 96 | 97 | var headers: Headers? 98 | 99 | var verify_ssl: Bool? 100 | 101 | var timeout: Int? 102 | 103 | var encoding: String? 104 | 105 | private var fileName = "" 106 | private var section = "Looker" 107 | 108 | init() { 109 | self.assign(DefaultSettings()) 110 | } 111 | 112 | init(_ settings: IApiSettings) { 113 | self.assign(settings) 114 | } 115 | 116 | // TODO figure out how to use Codable and PropertyListDecoder? 117 | // similar to https://www.raywenderlich.com/3418439-encoding-and-decoding-in-swift 118 | init(_ fileName: String, _ section: String = "Looker") throws { 119 | self.fileName = fileName 120 | self.section = section 121 | let config = parseConfig(fileName) 122 | let values = config[section] 123 | let defaults = DefaultSettings() 124 | self.base_url = values?["base_url"] as String? ?? defaults.base_url 125 | self.api_version = values?["api_version"] as String? ?? defaults.api_version 126 | self.headers = values?["headers"] as Any? ?? defaults.headers 127 | self.verify_ssl = values?["verify_ssl"]?.bool ?? defaults.verify_ssl 128 | self.timeout = Int((values?["timeout"])!) ?? defaults.timeout 129 | self.encoding = values?["encoding"] ?? defaults.encoding 130 | } 131 | 132 | mutating func assign(_ values: IApiSettings) { 133 | let defaults = DefaultSettings() 134 | self.base_url = values.base_url ?? defaults.base_url 135 | self.api_version = values.api_version ?? defaults.api_version 136 | self.headers = values.headers ?? defaults.headers 137 | self.verify_ssl = values.verify_ssl ?? defaults.verify_ssl 138 | self.timeout = values.timeout ?? defaults.timeout 139 | self.encoding = values.encoding ?? defaults.encoding 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /swift/sample-swift-sdk/sample-swift-sdk/LookerSwiftSDK/apiMethods.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2019 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 | import Foundation 26 | 27 | @available(OSX 10.15, *) 28 | class APIMethods { 29 | var authSession: IAuthorizer 30 | 31 | init(_ authSession: IAuthorizer) { 32 | self.authSession = authSession 33 | } 34 | 35 | func ok(_ response: SDKResponse) -> TSuccess { 36 | switch response { 37 | case .success(let response): 38 | return response 39 | case .error(let error): 40 | let message = error.errorDescription 41 | ?? error.failureReason 42 | ?? error.recoverySuggestion 43 | ?? error.helpAnchor 44 | ?? "Unknown SDK Error" 45 | print("Error: \(message)") 46 | } 47 | return () as! TSuccess 48 | // return SDKResponse.success(<#TSuccess#>) as! TSuccess 49 | } 50 | 51 | func authRequest( 52 | _ method: HttpMethod, 53 | _ path: String, 54 | _ queryParams: Values?, 55 | _ body: Any?, 56 | _ options: ITransportSettings? 57 | ) -> SDKResponse { 58 | return self.authSession.transport.request( 59 | method, 60 | path, 61 | queryParams, 62 | body, 63 | { props -> URLRequest in 64 | return self.authSession.authenticate(props) 65 | }, 66 | options 67 | ) 68 | } 69 | 70 | /** Make a GET request */ 71 | func get( 72 | _ path: String, 73 | _ queryParams: Values?, 74 | _ body: Any?, 75 | _ options: ITransportSettings? 76 | ) -> SDKResponse { 77 | return self.authRequest( 78 | HttpMethod.GET, 79 | path, 80 | queryParams, 81 | body, 82 | options 83 | ) 84 | } 85 | 86 | /** Make a HEAD request */ 87 | func head( 88 | _ path: String, 89 | _ queryParams: Values?, 90 | _ body: Any?, 91 | _ options: ITransportSettings? 92 | ) -> SDKResponse { 93 | return self.authRequest( 94 | HttpMethod.HEAD, 95 | path, 96 | queryParams, 97 | body, 98 | options 99 | ) 100 | } 101 | 102 | /** Make a DELETE request */ 103 | func delete( 104 | _ path: String, 105 | _ queryParams: Values?, 106 | _ body: Any?, 107 | _ options: ITransportSettings? 108 | ) -> SDKResponse { 109 | return self.authRequest( 110 | HttpMethod.DELETE, 111 | path, 112 | queryParams, 113 | body, 114 | options 115 | ) 116 | } 117 | 118 | /** Make a POST request */ 119 | func post( 120 | _ path: String, 121 | _ queryParams: Values?, 122 | _ body: Any?, 123 | _ options: ITransportSettings? 124 | ) -> SDKResponse { 125 | return self.authRequest( 126 | HttpMethod.POST, 127 | path, 128 | queryParams, 129 | body, 130 | options 131 | ) 132 | } 133 | 134 | /** Make a PUT request */ 135 | func put( 136 | _ path: String, 137 | _ queryParams: Values?, 138 | _ body: Any?, 139 | _ options: ITransportSettings? 140 | ) -> SDKResponse { 141 | return self.authRequest( 142 | HttpMethod.PUT, 143 | path, 144 | queryParams, 145 | body, 146 | options 147 | ) 148 | } 149 | 150 | /** Make a PATCH request */ 151 | func patch( 152 | _ path: String, 153 | _ queryParams: Values?, 154 | _ body: Any?, 155 | _ options: ITransportSettings? 156 | ) -> SDKResponse { 157 | return self.authRequest( 158 | HttpMethod.PATCH, 159 | path, 160 | queryParams, 161 | body, 162 | options 163 | ) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /swift/sample-swift-sdk/sample-swift-sdk/LookerSwiftSDK/apiSettings.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2019 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 | import Foundation 26 | 27 | let strLookerBaseUrl = "\(Constants.environmentPrefix)_BASE_URL" 28 | let strLookerApiVersion = "\(Constants.environmentPrefix)_API_VERSION" 29 | let strLookerVerifySsl = "\(Constants.environmentPrefix)_VERIFY_SSL" 30 | let strLookerTimeout = "\(Constants.environmentPrefix)_TIMEOUT" 31 | let strLookerClientId = "\(Constants.environmentPrefix)_CLIENT_ID" 32 | let strLookerClientSecret = "\(Constants.environmentPrefix)_CLIENT_SECRET" 33 | let strBadConfiguration = """ 34 | \(agentTag) configuration error: 35 | Missing required configuration values like base_url and api_version 36 | """ 37 | 38 | protocol IApiSettings: ITransportSettings { 39 | func isConfigured() -> Bool 40 | func readConfig(_ section: String?) -> IApiSection 41 | } 42 | 43 | typealias IApiSection = SectionConfig 44 | 45 | /** 46 | * default the runtime configuration settings 47 | * @constructor 48 | * 49 | */ 50 | struct DefaultSettings : IApiSettings { 51 | func isConfigured() -> Bool { 52 | return false 53 | } 54 | func readConfig(_ section: String? = nil) -> IApiSection { 55 | return [:] 56 | } 57 | var base_url: String? = ProcessInfo.processInfo.environment[strLookerBaseUrl] ?? "" 58 | var api_version: String? = ProcessInfo.processInfo.environment[strLookerApiVersion] ?? "3.1" 59 | var verify_ssl: Bool? = true 60 | var timeout: Int? = defaultTimeout 61 | var headers: Headers? 62 | var encoding: String? 63 | } 64 | 65 | /** 66 | * Read any key/value collection for environment variable names and return as IApiSettings 67 | * @constructor 68 | * 69 | * The values keys are: 70 | * - _BASE_URL 71 | * - _API_VERSION 72 | * - _CLIENT_ID 73 | * - _CLIENT_SECRET 74 | * - _VERIFY_SSL 75 | * - _TIMEOUT 76 | */ 77 | func ValueSettings(_ values: StringDictionary) -> IApiSettings { 78 | var settings = DefaultSettings() 79 | settings.api_version = values[strLookerApiVersion] ?? settings.api_version 80 | settings.base_url = values[strLookerBaseUrl] ?? settings.base_url 81 | if (values[strLookerVerifySsl] != nil) { 82 | let v = values[strLookerVerifySsl]!.lowercased() 83 | settings.verify_ssl = v == "true" || v == "1" 84 | } 85 | if (values[strLookerTimeout] != nil) { 86 | settings.timeout = Int(values[strLookerTimeout]!)! 87 | } 88 | return settings 89 | } 90 | 91 | /** 92 | * @struct ApiSettings 93 | * 94 | * .ini Configuration initializer 95 | */ 96 | struct ApiSettings: IApiSettings { 97 | func readConfig(_ section: String? = nil) -> IApiSection { 98 | return [:] 99 | } 100 | 101 | var base_url: String? 102 | var api_version: String? 103 | var verify_ssl: Bool? 104 | var timeout: Int? 105 | var headers: Headers? 106 | var encoding: String? 107 | 108 | init() { 109 | 110 | } 111 | 112 | init(_ settings: IApiSettings) throws { 113 | let defaults = DefaultSettings() 114 | // coerce types to declared types since some paths could have non-conforming settings values 115 | self.base_url = settings.base_url ?? defaults.base_url 116 | self.api_version = settings.api_version ?? defaults.api_version 117 | self.verify_ssl = settings.verify_ssl ?? defaults.verify_ssl 118 | self.timeout = settings.timeout ?? defaults.timeout 119 | self.headers = settings.headers ?? defaults.headers 120 | self.encoding = settings.encoding ?? defaults.encoding 121 | if (!self.isConfigured()) { 122 | throw SDKError(strBadConfiguration) 123 | } 124 | } 125 | 126 | func isConfigured() -> Bool { 127 | return (self.base_url != "" && self.api_version != "") 128 | } 129 | } 130 | 131 | -------------------------------------------------------------------------------- /swift/sample-swift-sdk/sample-swift-sdk/LookerSwiftSDK/authSession.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2019 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 | import Foundation 26 | 27 | protocol IAuthSession: IAuthorizer { 28 | var sudoId: String { get set } 29 | func getToken() -> AuthToken 30 | func isSudo() -> Bool 31 | func login(_ sudoId : String) -> AuthToken 32 | func reset() -> Void 33 | } 34 | 35 | @available(OSX 10.15, *) 36 | class AuthSession: IAuthSession { 37 | 38 | var _authToken = AuthToken() 39 | var _sudoToken = AuthToken() 40 | var sudoId: String = "" 41 | var settings: IApiSettings 42 | var transport: ITransport 43 | var activeToken: AuthToken { 44 | get { 45 | if (!self._sudoToken.access_token.isEmpty) { 46 | return self._sudoToken 47 | } 48 | return self._authToken 49 | } 50 | } 51 | 52 | init(_ settings: IApiSettings, _ transport: ITransport? = nil) { 53 | self.settings = settings 54 | self.transport = transport ?? BaseTransport(settings) 55 | } 56 | 57 | func getToken() -> AuthToken { 58 | if (!self.isAuthenticated()) { 59 | // this is currently a synchronous call so unblocking 60 | return self.login() 61 | } 62 | return self.activeToken 63 | } 64 | 65 | func isSudo() -> Bool { 66 | return ((!self.sudoId.isEmpty) && self._sudoToken.isActive()) 67 | } 68 | 69 | func login(_ sudoId: String = "") -> AuthToken { 70 | if (sudoId != self.sudoId || !self.isAuthenticated()) { 71 | _ = try? self._login(sudoId) 72 | } 73 | return self.activeToken 74 | } 75 | 76 | func reset() { 77 | self.sudoId = "" 78 | self._authToken.reset() 79 | self._sudoToken.reset() 80 | } 81 | 82 | func isAuthenticated() -> Bool { 83 | return self.activeToken.isActive() 84 | } 85 | 86 | func authenticate(_ props: URLRequest) -> URLRequest { 87 | let token = self.getToken() 88 | var updated = props 89 | if (token.isActive()) { 90 | updated.addValue("Bearer \(token.access_token)", forHTTPHeaderField: "Authorization") 91 | } 92 | return updated 93 | } 94 | 95 | func logout() -> Bool { 96 | var result = false 97 | if (self.isAuthenticated()) { 98 | result = self._logout() 99 | } 100 | return result 101 | } 102 | 103 | private func ok(_ response: SDKResponse) throws -> TSuccess { 104 | switch response { 105 | case .success(let response): 106 | return response 107 | case .error(let error): 108 | throw SDKError(error.errorDescription 109 | ?? error.failureReason 110 | ?? error.recoverySuggestion 111 | ?? error.helpAnchor 112 | ?? "Unknown SDK Error") 113 | } 114 | } 115 | 116 | private func _login(_ newId: String = "") throws -> AuthToken { 117 | // If we've got sudo logged in, log 'em out 118 | _ = self.sudoLogout() 119 | 120 | if (newId != self.sudoId) { 121 | self.sudoId = newId 122 | } 123 | 124 | if (!self._authToken.isActive()) { 125 | self.reset() 126 | let section = self.settings.readConfig(nil) 127 | let client_id = ProcessInfo.processInfo.environment[strLookerClientId] ?? section["client_id"] 128 | let client_secret = ProcessInfo.processInfo.environment[strLookerClientSecret] ?? section["client_secret"] 129 | if ((client_id ?? "").isEmpty || (client_secret ?? "").isEmpty) { 130 | throw SDKError("API credentials client_id and/or client_secret are not set") 131 | } 132 | let response : SDKResponse = self.transport.request( 133 | HttpMethod.POST, 134 | "/login", 135 | ["client_id": client_id!, "client_secret": client_secret!], 136 | nil, 137 | nil, 138 | nil 139 | ) 140 | let token = try? self.ok(response) 141 | _ = self._authToken.setToken(token!) 142 | } 143 | 144 | if (!self.sudoId.isEmpty) { 145 | let token = self.activeToken 146 | let response : SDKResponse = self.transport.request( 147 | HttpMethod.POST, 148 | "/login/\(newId)", 149 | nil, 150 | nil, 151 | { props -> URLRequest in 152 | var update = props 153 | update.addValue("Bearer \(token.access_token)", forHTTPHeaderField: "Authorization") 154 | return update 155 | }, 156 | self.settings 157 | ) 158 | let sudoToken = try? self.ok(response) 159 | _ = self._sudoToken.setToken(sudoToken!) 160 | } 161 | return self.activeToken 162 | } 163 | 164 | private func sudoLogout() -> Bool { 165 | var result = false 166 | if (self.isSudo()) { 167 | result = self.logout() 168 | self._sudoToken.reset() 169 | } 170 | return result 171 | } 172 | 173 | private func _logout() -> Bool { 174 | var result = true 175 | let token = self.activeToken 176 | let response : SDKResponse = self.transport.request( 177 | HttpMethod.DELETE, 178 | "/logout", 179 | nil, 180 | nil, 181 | { props -> URLRequest in 182 | var update = props 183 | update.addValue("Bearer \(token.access_token)", forHTTPHeaderField: "Authorization") 184 | return update 185 | }, 186 | self.settings 187 | ) 188 | do { 189 | _ = try self.ok(response) 190 | if (!self.sudoId.isEmpty) { 191 | self.sudoId = "" 192 | self._sudoToken.reset() 193 | if (!self._authToken.isActive()) { 194 | _ = self.login() 195 | } 196 | } else { 197 | self.reset() 198 | } 199 | } catch { 200 | result = false 201 | } 202 | return result 203 | } 204 | 205 | } 206 | -------------------------------------------------------------------------------- /swift/sample-swift-sdk/sample-swift-sdk/LookerSwiftSDK/authToken.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2019 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 | import Foundation 26 | 27 | protocol AccessTokenProtocol { 28 | var access_token: String { get set } 29 | var token_type: String { get set } 30 | var expires_in: Int64 { get set } 31 | } 32 | // 33 | //struct AccessToken: AccessTokenProtocol, Codable { 34 | // var access_token: String 35 | // var token_type: String 36 | // var expires_in: Int 37 | //} 38 | 39 | struct AuthToken: AccessTokenProtocol { 40 | var access_token: String = "" 41 | var token_type: String = "" 42 | var expires_in: Int64 = 0 43 | 44 | private var expiresAt: Date? 45 | 46 | init() { } 47 | 48 | init(_ token: AccessToken) { 49 | self = self.setToken(token) 50 | } 51 | 52 | // true if the authentication token is set and has not timed without 53 | func isActive() -> Bool { 54 | if (self.access_token == "" || self.expires_in == 0) { return false } 55 | guard let expiresAt = self.expiresAt else { return false } 56 | return expiresAt > Date() 57 | } 58 | 59 | static func expiryDate(_ inSeconds: Int) -> Date { 60 | let interval = inSeconds > 0 ? inSeconds : -10 61 | return Date.init(timeIntervalSinceNow: TimeInterval(interval)) 62 | } 63 | 64 | // Assign the token and set its expiration 65 | mutating func setToken(_ token: AccessToken) -> Self { 66 | self.access_token = token.access_token! 67 | self.token_type = token.token_type! 68 | self.expires_in = token.expires_in! 69 | self.expiresAt = Self.expiryDate(Int(self.expires_in)) 70 | return self 71 | } 72 | 73 | mutating func reset() { 74 | self.access_token = "" 75 | self.expires_in = 0 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /swift/sample-swift-sdk/sample-swift-sdk/LookerSwiftSDK/looker.swift: -------------------------------------------------------------------------------- 1 | struct looker { 2 | var text = "Hello, SDK World!" 3 | } 4 | -------------------------------------------------------------------------------- /swift/sample-swift-sdk/sample-swift-sdk/MainView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // sample-swift-sdk 4 | // 5 | 6 | import SwiftUI 7 | 8 | struct MainView: View { 9 | @State var dashboards = Dashboards().all 10 | 11 | var body: some View { 12 | List(dashboards) { dashboard in 13 | HStack() { 14 | Text(String(dashboard.id)) 15 | Text(dashboard.title) 16 | } 17 | } 18 | } 19 | } 20 | 21 | struct MainView_Previews: PreviewProvider { 22 | static var previews: some View { 23 | MainView() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /swift/sample-swift-sdk/sample-swift-sdk/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /swift/sample-swift-sdk/sample-swift-sdk/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // sample-swift-sdk 4 | // 5 | 6 | import UIKit 7 | import SwiftUI 8 | 9 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 10 | 11 | var window: UIWindow? 12 | 13 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 14 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 15 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 16 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 17 | 18 | // Create the SwiftUI view that provides the window contents. 19 | let mainView = MainView() 20 | 21 | // Use a UIHostingController as window root view controller. 22 | if let windowScene = scene as? UIWindowScene { 23 | let window = UIWindow(windowScene: windowScene) 24 | window.rootViewController = UIHostingController(rootView: mainView) 25 | self.window = window 26 | window.makeKeyAndVisible() 27 | } 28 | } 29 | 30 | func sceneDidDisconnect(_ scene: UIScene) { 31 | // Called as the scene is being released by the system. 32 | // This occurs shortly after the scene enters the background, or when its session is discarded. 33 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 34 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 35 | } 36 | 37 | func sceneDidBecomeActive(_ scene: UIScene) { 38 | // Called when the scene has moved from an inactive state to an active state. 39 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 40 | } 41 | 42 | func sceneWillResignActive(_ scene: UIScene) { 43 | // Called when the scene will move from an active state to an inactive state. 44 | // This may occur due to temporary interruptions (ex. an incoming phone call). 45 | } 46 | 47 | func sceneWillEnterForeground(_ scene: UIScene) { 48 | // Called as the scene transitions from the background to the foreground. 49 | // Use this method to undo the changes made on entering the background. 50 | } 51 | 52 | func sceneDidEnterBackground(_ scene: UIScene) { 53 | // Called as the scene transitions from the foreground to the background. 54 | // Use this method to save data, release shared resources, and store enough scene-specific state information 55 | // to restore the scene back to its current state. 56 | } 57 | 58 | 59 | } 60 | 61 | -------------------------------------------------------------------------------- /swift/sample-swift-sdk/sample-swift-sdkTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /swift/sample-swift-sdk/sample-swift-sdkTests/sample_swift_sdkTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // sample_swift_sdkTests.swift 3 | // sample-swift-sdkTests 4 | // 5 | 6 | 7 | import XCTest 8 | @testable import sample_swift_sdk 9 | 10 | class sample_swift_sdkTests: XCTestCase { 11 | 12 | override func setUp() { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | } 15 | 16 | override func tearDown() { 17 | // Put teardown code here. This method is called after the invocation of each test method in the class. 18 | } 19 | 20 | func testExample() { 21 | // This is an example of a functional test case. 22 | // Use XCTAssert and related functions to verify your tests produce the correct results. 23 | } 24 | 25 | func testPerformanceExample() { 26 | // This is an example of a performance test case. 27 | self.measure { 28 | // Put the code you want to measure the time of here. 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /swift/sample-swift-sdk/sample-swift-sdkUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /swift/sample-swift-sdk/sample-swift-sdkUITests/sample_swift_sdkUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // sample_swift_sdkUITests.swift 3 | // sample-swift-sdkUITests 4 | // 5 | 6 | import XCTest 7 | 8 | class sample_swift_sdkUITests: XCTestCase { 9 | 10 | override func setUp() { 11 | // Put setup code here. This method is called before the invocation of each test method in the class. 12 | 13 | // In UI tests it is usually best to stop immediately when a failure occurs. 14 | continueAfterFailure = false 15 | 16 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | } 22 | 23 | func testExample() { 24 | // UI tests must launch the application that they test. 25 | let app = XCUIApplication() 26 | app.launch() 27 | 28 | // Use recording to get started writing UI tests. 29 | // Use XCTAssert and related functions to verify your tests produce the correct results. 30 | } 31 | 32 | func testLaunchPerformance() { 33 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { 34 | // This measures how long it takes to launch your application. 35 | measure(metrics: [XCTOSSignpostMetric.applicationLaunch]) { 36 | XCUIApplication().launch() 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /typescript/README.md: -------------------------------------------------------------------------------- 1 | # Typescript Examples for the Looker API 2 | 3 | The examples in this folder use `yarn`. If you don't have `yarn` installed already, you'll need to [install](https://yarnpkg.com/en/docs/install) it. 4 | 5 | ## Using yarn 6 | 7 | Use 8 | 9 | ```bash 10 | yarn ts-node {filename} [other command-line options] 11 | ``` 12 | 13 | to run a Typescript SDK example file. 14 | 15 | ## Example list 16 | 17 | Very brief descriptions of the examples and the link to the main example/project file are provided here. For more information on an example, look at the source file. 18 | 19 | ### General examples 20 | 21 | |   Example Topic   | Discussion | 22 | | ------------- | ---------- | 23 | | [multiple APIs](dual.ts) | A simple example that shows how to use multiple versions of the API in the same file. The goal is to make iterative migration from an older API to a newer API in existing SDK code easier. | 24 | | [custom configReader](customConfigReader.ts) | Shows how to implement a custom method of reading your configuration settings, including API credentials | 25 | | [SDK utilities](utils.ts) | This file has some SDK utility methods tasks like:
  • finding a dashboard by name
  • listing dashboards and ids
  • finding a tile on a dashboard
  • listing queryable tiles for a dashboard
  • a general-purpose `waitForRender` routine with progress ticks
| 26 | 27 | ### User management 28 | 29 | |   Example Topic   | Discussion | 30 | | ------------- | ---------- | 31 | | [sudo as user](sudoAsUser.ts) | Uses several User management SDK methods and shows how to `sudo` as a different user than the default API credentials user. Once the auth session is set to that user, subsequent SDK requests will be "as user ``" when submitted to the API. | 32 | 33 | 34 | ### Downloading 35 | 36 | |   Example Topic   | Discussion | 37 | | ------------- | ---------- | 38 | | [download a dashboard tile by name](downloadTile.ts) | Find the requested dashboard by name, then the requested tile by name. If either name matches, the list of all available items is display. Supported output formats are PNG, JPG, CSV, JSON, and anything else supported by the `run_query` endpoint. This sample shows progress during a render task, and also how to use the SDK's streaming support. | 39 | | [download a dashboard by name](downloadDashboard.ts) | Find the requested dashboard by name, then render it in the requested format. Default render format is PDF. PDF or PNG are the recommended output formats, but JPG is also supported. | 40 | -------------------------------------------------------------------------------- /typescript/customConfigReader.ts: -------------------------------------------------------------------------------- 1 | import { 2 | LookerNodeSDK, 3 | NodeSettings, 4 | IApiSettings, 5 | IApiSection 6 | } from "@looker/sdk" 7 | 8 | /** 9 | * @class customConfigReader 10 | * 11 | * A custom configuration reader that overrides the readConfig() method 12 | * in NodeSettings to allow fetching client_id and client_secret 13 | * from anywhere. 14 | */ 15 | class customConfigReader extends NodeSettings { 16 | constructor(settings: IApiSettings) { 17 | super(settings) 18 | } 19 | /** 20 | * @returns an IApiSection object containing client_id and client_secret 21 | */ 22 | readConfig(): IApiSection { 23 | return { 24 | client_id: "clientId", 25 | client_secret: "clientSecret" 26 | } 27 | } 28 | } 29 | 30 | (async () => { 31 | const sdk = LookerNodeSDK.init31( 32 | new customConfigReader({ 33 | base_url: "https://:19999" 34 | } as IApiSettings) 35 | ) 36 | 37 | const me = await sdk.ok( 38 | sdk.me( 39 | "id, first_name, last_name, display_name, email, personal_space_id, home_space_id, group_ids, role_ids" 40 | ) 41 | ) 42 | console.log({ me }) 43 | })() 44 | -------------------------------------------------------------------------------- /typescript/downloadDashboard.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2019 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 | import { 26 | NodeSettingsIniFile, 27 | NodeSession, 28 | Looker40SDK as LookerSDK, 29 | IDashboard, 30 | IRequestCreateDashboardRenderTask 31 | } from '@looker/sdk' 32 | import * as fs from 'fs' 33 | import { getDashboard, waitForRender } from './utils' 34 | 35 | /** 36 | * 37 | * @type {string} Local configuration file name, one directory above 38 | */ 39 | const localConfig = '../looker.ini' 40 | 41 | /** 42 | * 43 | * @type {NodeSettingsIniFile} Settings retrieved from the configuration file 44 | */ 45 | const settings = new NodeSettingsIniFile(localConfig, 'Looker') 46 | 47 | /** 48 | * Automatic authentication support for the Node SDK 49 | * @type {NodeSession} Initialized node-based session manager 50 | */ 51 | const session = new NodeSession(settings) 52 | 53 | /** 54 | * Initialized SDK object 55 | * @type {LookerSDK} SDK object configured for use with Node 56 | */ 57 | const sdk = new LookerSDK(session) 58 | 59 | /** 60 | * Read command-line parameters. Still have a bug for png argument 61 | * @returns {{dashboardTitle: string, renderFormat: string}} 62 | */ 63 | const getParams = () => { 64 | const offset = 1 65 | return { 66 | dashboardTitle: 67 | process.argv.length > offset + 1 ? process.argv[offset + 1] : '', 68 | outputFormat: 69 | process.argv.length > offset + 2 ? process.argv[offset + 2] : 'pdf' 70 | } 71 | } 72 | 73 | /** 74 | * Download a dashboard using a render task 75 | * @param {LookerSDK} sdk initialized Looker SDK 76 | * @param {IDashboard} dashboard to download 77 | * @param {string} format format of rendering 78 | * @returns {Promise} Name of file downloaded 79 | */ 80 | const downloadDashboard = async ( 81 | sdk: LookerSDK, 82 | dashboard: IDashboard, 83 | format: string 84 | ) => { 85 | let fileName = undefined 86 | try { 87 | const req: IRequestCreateDashboardRenderTask = { 88 | dashboard_id: dashboard.id!, 89 | result_format: format, 90 | body: {}, 91 | width: 1920, 92 | height: 1080 93 | } 94 | const task = await sdk.ok(sdk.create_dashboard_render_task(req)) 95 | 96 | if (!task || !task.id) { 97 | console.error(`Could not create a render task for ${dashboard.title}`) 98 | return 99 | } 100 | 101 | const result = await waitForRender(sdk, task.id!) 102 | if (result) { 103 | fileName = `${dashboard.title}.${format}` 104 | fs.writeFile(fileName, result, 'binary', err => { 105 | if (err) { 106 | fileName = undefined 107 | console.error(err) 108 | } 109 | }) 110 | } 111 | } catch (err) { 112 | console.error(`'${format}' is probably not a valid format`) 113 | console.error(err) 114 | } 115 | return fileName 116 | } 117 | ;(async () => { 118 | const { dashboardTitle, outputFormat } = getParams() 119 | if (!dashboardTitle) { 120 | console.warn('Please provide: []') 121 | console.warn( 122 | ' outputFormat defaults to "pdf". png and jpg are also supported.' 123 | ) 124 | return 125 | } 126 | console.log(`Rendering dashboard "${dashboardTitle}" as ${outputFormat} ...`) 127 | 128 | const dashboard = await getDashboard(sdk, dashboardTitle) 129 | if (dashboard) { 130 | const fileName = await downloadDashboard(sdk, dashboard, outputFormat) 131 | console.log(`open "${fileName}" to see the download`) 132 | } 133 | 134 | await sdk.authSession.logout() // logout of API session 135 | if (!sdk.authSession.isAuthenticated()) { 136 | console.log('Logout successful') 137 | } 138 | })() 139 | -------------------------------------------------------------------------------- /typescript/dual.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2019 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 | import { Looker40SDK, Looker31SDK, NodeSession, NodeSettingsIniFile } from '@looker/sdk' 26 | 27 | /** 28 | * 29 | * @type {string} Local configuration file name, one directory above 30 | */ 31 | const localConfig = '../looker.ini' 32 | const settings = new NodeSettingsIniFile(localConfig, "Looker") 33 | const session = new NodeSession(settings) 34 | const sdk = new Looker40SDK(session) 35 | const sdk31 = new Looker31SDK(session); 36 | 37 | (async () => { 38 | const [me40, me31] = await Promise.all([sdk.ok(sdk.me()), sdk.ok(sdk31.me())]) 39 | if (me40.id == me31.id) { 40 | console.log('Congratulations! You are using dual SDKs!') 41 | } 42 | })() 43 | -------------------------------------------------------------------------------- /typescript/index.ts: -------------------------------------------------------------------------------- 1 | console.log("Please read the README.md") 2 | -------------------------------------------------------------------------------- /typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sdkexamples", 3 | "version": "1.0.0", 4 | "description": "Looker SDK examples in Typescript/Javascript", 5 | "main": "index.ts", 6 | "repository": "https://github.com/looker-open-source/sdk-examples/tree/master/typescript", 7 | "author": "Looker", 8 | "license": "MIT", 9 | "private": false, 10 | "dependencies": { 11 | "@looker/sdk": "^0.3.0-beta.1", 12 | "ts-node": "^8.4.1" 13 | }, 14 | "devDependencies": { 15 | "@types/node": "^14.0.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /typescript/sudoAsUser.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2019 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 | import { 26 | NodeSettingsIniFile, 27 | NodeSession, 28 | Looker40SDK as LookerSDK, 29 | IAuthSession 30 | } from '@looker/sdk' 31 | 32 | /** 33 | * 34 | * @type {string} Local configuration file name, one directory above 35 | */ 36 | const localConfig = '../looker.ini' 37 | 38 | /** 39 | * 40 | * @type {NodeSettingsIniFile} Settings retrieved from the configuration file 41 | */ 42 | const settings = new NodeSettingsIniFile(localConfig, 'Looker') 43 | 44 | /** 45 | * Automatic authentication support for the Node SDK 46 | * @type {NodeSession} Initialized node-based session manager 47 | */ 48 | const session = new NodeSession(settings) 49 | 50 | /** 51 | * Initialized SDK object 52 | * @type {LookerSDK} SDK object configured for use with Node 53 | */ 54 | const sdk = new LookerSDK(session) 55 | 56 | /** 57 | * 58 | * @type {string} email matching pattern for searching users 59 | */ 60 | const matchDomain = '%@looker.com' 61 | 62 | /** 63 | * Find a different user than the specified user 64 | * @param {number} userId id to exclude from match 65 | * @param {string} emailPattern email pattern for user search 66 | * @returns {Promise} Returns the first matched user, or undefined if no match 67 | */ 68 | const anyoneButMe = async (userId: number, emailPattern: string) => { 69 | const all = await sdk.ok( 70 | sdk.search_users({ email: emailPattern, page: 1, per_page: 2 }) 71 | ) 72 | if (!all || all.length === 0) { 73 | console.warn(`No matches for ${emailPattern}`) 74 | return undefined 75 | } 76 | // find a user who is not the specified user 77 | const [other] = all.filter(u => u.id !== userId && !u.is_disabled).slice(0, 1) 78 | return other 79 | } 80 | ;(async () => { 81 | const userFields = 82 | 'id, first_name, last_name, display_name, email, personal_space_id, home_space_id, group_ids, role_ids' 83 | // retrieve your user account to verify correct credentials 84 | const me = await sdk.ok(sdk.me(userFields)) 85 | if (!me) { 86 | console.warn('API authentication failed') 87 | return 88 | } 89 | console.log({ me }) 90 | const sudoUser = await anyoneButMe(me.id!, matchDomain) 91 | if (sudoUser) { 92 | const auth = sdk.authSession as IAuthSession 93 | await auth.login(sudoUser.id) 94 | const sudo = await sdk.ok(sdk.me(userFields)) 95 | console.log({ sudo }) 96 | await sdk.authSession.logout() // logout of sudo 97 | } 98 | 99 | await sdk.authSession.logout() // logout of API session 100 | if (!sdk.authSession.isAuthenticated()) { 101 | console.log('Logout successful') 102 | } 103 | })() 104 | -------------------------------------------------------------------------------- /typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "noFallthroughCasesInSwitch": true, 5 | "noImplicitReturns": true, 6 | "noUnusedLocals": true, 7 | "noUnusedParameters": true, 8 | "esModuleInterop": true, 9 | "plugins": [ 10 | { 11 | "name": "tslint-language-service", 12 | "alwaysShowRuleFailuresAsWarnings": true 13 | } 14 | ], 15 | "skipLibCheck": true, 16 | "strict": true, 17 | "module": "commonjs", 18 | "moduleResolution": "node", 19 | "target": "es6", 20 | "lib": ["es6", "es2017.object", "dom"], 21 | "noImplicitAny": true, 22 | "baseUrl": "./" 23 | }, 24 | "include": ["./*.ts", "./src/*.ts"] 25 | } 26 | -------------------------------------------------------------------------------- /typescript/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-config-standard", 4 | "tslint-config-prettier" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "interface-name": [true, "always-prefix"], 9 | "jsx-boolean-value": false, 10 | "no-namespace": [true, "allow-declarations"] 11 | }, 12 | "rulesDirectory": [] 13 | } 14 | -------------------------------------------------------------------------------- /typescript/utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2019 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 | import { Looker40SDK as LookerSDK, IDashboard, IRenderTask } from '@looker/sdk' 26 | 27 | /** 28 | * Find a dashboard by title 29 | * @param sdk Looker SDK object 30 | * @param {string} title of dashboard 31 | * @returns {Promise} the matched dashboard 32 | */ 33 | export const getDashboard = async (sdk: LookerSDK, title: string) => { 34 | const [dash] = await sdk.ok(sdk.search_dashboards({title})) 35 | if (!dash) { 36 | console.warn(`No dashboard titled "${title}" was found`) 37 | const all = await sdk.ok(sdk.all_dashboards("id,title")) 38 | const titles = all.map(t => `${t.id}:${t.title}`) 39 | console.log(`Available dashboards are:\n${titles.join('\n')}\n`) 40 | } 41 | return dash 42 | } 43 | 44 | /** 45 | * Get a tile by title from a dashboard 46 | * @param {IDashboard} dash Dashboard to search 47 | * @param {string} title Title title to find 48 | * @returns {IDashboardElement | undefined} Returns the found tile or undefined 49 | */ 50 | export const getDashboardTile = (dash: IDashboard, title: string) => { 51 | title = title.toLowerCase() 52 | if (!dash.dashboard_elements) return undefined 53 | const [tile]= dash.dashboard_elements.filter(t => String(t.title).toLowerCase() === title) 54 | if (!tile) { 55 | console.warn(`No tile titled "${title}" found on Dashboard "${dash.title}"`) 56 | const tiles = dash.dashboard_elements 57 | .filter(t => typeof t.query_id == 'number') 58 | .map(t => t.title) 59 | console.log(`Available tiles with queries are:\n${tiles.join("\n")}\n`) 60 | } 61 | return tile 62 | } 63 | 64 | /** 65 | * Wait specified milliseconds 66 | * @param {number} ms time in milliseconds 67 | * @returns {Promise} promise timeout 68 | */ 69 | export const sleep = async (ms: number) => { 70 | return new Promise(resolve =>{ 71 | setTimeout(resolve, ms) 72 | }) 73 | } 74 | 75 | /** 76 | * Progress ticker callback type 77 | */ 78 | export type ProgressTicker = ( 79 | elapsed: number, 80 | message: string | IRenderTask 81 | ) => void 82 | 83 | /** 84 | * Default render progress tick reporter 85 | * @param {number} elapsed Number of seconds elapsed for render task 86 | * @param {string | IRenderTask} message Message to output or render task poll (if failed) 87 | */ 88 | const defaultProgress = (elapsed: number, message: string | IRenderTask) => { 89 | if (typeof message == 'string') { 90 | if (elapsed >= 0) { 91 | console.log(`${elapsed} seconds elapsed ${message}`) 92 | } else { 93 | console.log(message) 94 | } 95 | } else { 96 | // expected to be a poll if we got here 97 | const poll = message as IRenderTask 98 | if (poll.status == 'failure') { 99 | console.error('Render failed. Details:') 100 | console.error({poll}) 101 | const err = new Error() 102 | err.message = `${poll.status}: ${poll.status_detail} for ${poll.result_format} render by User ID ${poll.user_id}` 103 | throw err 104 | } 105 | } 106 | } 107 | 108 | /** 109 | * General-purpose "wait for render task to complete" function 110 | * @param {LookerSDK} sdk LookerSDK object 111 | * @param {string} taskId render task id 112 | * @param {ProgressTicker} progressTick callback for progress ticks 113 | * @param {number} pause number of seconds to wait before checking progress. Defaults to 0.5 114 | * @returns {Promise} Results of render task. Can be a binary string 115 | */ 116 | export const waitForRender = async ( 117 | sdk: LookerSDK, 118 | taskId: string, 119 | progressTick: ProgressTicker = defaultProgress, 120 | pause: number = 0.5 121 | ) => { 122 | // poll the render task until it completes 123 | let elapsed = 0.0 124 | const delay = pause * 1000 // convert seconds to milliseconds 125 | while (true) { 126 | const poll = await sdk.ok(sdk.render_task(taskId)) 127 | if (poll.status === 'failure') { 128 | progressTick(-1, poll) 129 | return 130 | } 131 | if (poll.status === 'success') { 132 | break 133 | } 134 | await sleep(delay) 135 | progressTick(elapsed += pause, '') 136 | } 137 | return await sdk.ok(sdk.render_task_results(taskId)) 138 | } 139 | --------------------------------------------------------------------------------