generator.lookml

Generate lookml from namespaces.

  1"""Generate lookml from namespaces."""
  2
  3import logging
  4from pathlib import Path
  5from typing import Dict, Iterable, Optional
  6
  7import click
  8import lkml
  9import yaml
 10from google.cloud import bigquery
 11
 12from .dashboards import DASHBOARD_TYPES
 13from .explores import EXPLORE_TYPES
 14from .metrics_utils import LOOKER_METRIC_HUB_REPO, METRIC_HUB_REPO, MetricsConfigLoader
 15from .namespaces import _get_glean_apps
 16from .views import VIEW_TYPES, View, ViewDict
 17from .views.datagroups import generate_datagroups
 18
 19FILE_HEADER = """
 20# *Do not manually modify this file*
 21#
 22# This file has been generated via https://github.com/mozilla/lookml-generator
 23# You can extend this view in the looker-spoke-default project (https://github.com/mozilla/looker-spoke-default)
 24
 25"""
 26
 27
 28def _generate_views(
 29    client, out_dir: Path, views: Iterable[View], v1_name: Optional[str]
 30) -> Iterable[Path]:
 31    for view in views:
 32        logging.info(
 33            f"Generating lookml for view {view.name} in {view.namespace} of type {view.view_type}"
 34        )
 35        path = out_dir / f"{view.name}.view.lkml"
 36        lookml = view.to_lookml(client, v1_name)
 37        if lookml == {}:
 38            continue
 39
 40        # lkml.dump may return None, in which case write an empty file
 41        path.write_text(FILE_HEADER + (lkml.dump(lookml) or ""))
 42        yield path
 43
 44
 45def _generate_explores(
 46    client,
 47    out_dir: Path,
 48    namespace: str,
 49    explores: dict,
 50    views_dir: Path,
 51    v1_name: Optional[
 52        str
 53    ],  # v1_name for Glean explores: see: https://mozilla.github.io/probe-scraper/#tag/library
 54) -> Iterable[Path]:
 55    for explore_name, defn in explores.items():
 56        logging.info(f"Generating lookml for explore {explore_name} in {namespace}")
 57        explore = EXPLORE_TYPES[defn["type"]].from_dict(explore_name, defn, views_dir)
 58        file_lookml = {
 59            # Looker validates all included files,
 60            # so if we're not explicit about files here, validation takes
 61            # forever as looker re-validates all views for every explore (if we used *).
 62            "includes": [
 63                f"/looker-hub/{namespace}/views/{view}.view.lkml"
 64                for view in explore.get_dependent_views()
 65            ],
 66            "explores": explore.to_lookml(client, v1_name),
 67        }
 68        path = out_dir / (explore_name + ".explore.lkml")
 69        # lkml.dump may return None, in which case write an empty file
 70        path.write_text(FILE_HEADER + (lkml.dump(file_lookml) or ""))
 71        yield path
 72
 73
 74def _generate_dashboards(
 75    client,
 76    dash_dir: Path,
 77    namespace: str,
 78    dashboards: dict,
 79):
 80    for dashboard_name, dashboard_info in dashboards.items():
 81        logging.info(f"Generating lookml for dashboard {dashboard_name} in {namespace}")
 82        dashboard = DASHBOARD_TYPES[dashboard_info["type"]].from_dict(
 83            namespace, dashboard_name, dashboard_info
 84        )
 85
 86        dashboard_lookml = dashboard.to_lookml(client)
 87        dash_path = dash_dir / f"{dashboard_name}.dashboard.lookml"
 88        dash_path.write_text(FILE_HEADER + dashboard_lookml)
 89        yield dash_path
 90
 91
 92def _get_views_from_dict(views: Dict[str, ViewDict], namespace: str) -> Iterable[View]:
 93    for view_name, view_info in views.items():
 94        yield VIEW_TYPES[view_info["type"]].from_dict(  # type: ignore
 95            namespace, view_name, view_info
 96        )
 97
 98
 99def _glean_apps_to_v1_map(glean_apps):
100    return {d["name"]: d["v1_name"] for d in glean_apps}
101
102
103def _lookml(namespaces, glean_apps, target_dir, namespace_filter=[]):
104    client = bigquery.Client()
105
106    namespaces_content = namespaces.read()
107    _namespaces = yaml.safe_load(namespaces_content)
108    target = Path(target_dir)
109    target.mkdir(parents=True, exist_ok=True)
110
111    # Write namespaces file to target directory, for use
112    # by the Glean Dictionary and other tools
113    with open(target / "namespaces.yaml", "w") as target_namespaces_file:
114        target_namespaces_file.write(namespaces_content)
115
116    v1_mapping = _glean_apps_to_v1_map(glean_apps)
117    for namespace, lookml_objects in _namespaces.items():
118        if len(namespace_filter) == 0 or namespace in namespace_filter:
119            logging.info(f"\nGenerating namespace {namespace}")
120
121            view_dir = target / namespace / "views"
122            view_dir.mkdir(parents=True, exist_ok=True)
123            views = list(
124                _get_views_from_dict(lookml_objects.get("views", {}), namespace)
125            )
126
127            logging.info("  Generating views")
128            v1_name: Optional[str] = v1_mapping.get(namespace)
129            for view_path in _generate_views(client, view_dir, views, v1_name):
130                logging.info(f"    ...Generating {view_path}")
131
132            logging.info("  Generating datagroups")
133            generate_datagroups(views, target, namespace, client)
134
135            explore_dir = target / namespace / "explores"
136            explore_dir.mkdir(parents=True, exist_ok=True)
137            explores = lookml_objects.get("explores", {})
138            logging.info("  Generating explores")
139            for explore_path in _generate_explores(
140                client, explore_dir, namespace, explores, view_dir, v1_name
141            ):
142                logging.info(f"    ...Generating {explore_path}")
143
144            logging.info("  Generating dashboards")
145            dashboard_dir = target / namespace / "dashboards"
146            dashboard_dir.mkdir(parents=True, exist_ok=True)
147            dashboards = lookml_objects.get("dashboards", {})
148            for dashboard_path in _generate_dashboards(
149                client, dashboard_dir, namespace, dashboards
150            ):
151                logging.info(f"    ...Generating {dashboard_path}")
152
153
154@click.command(help=__doc__)
155@click.option(
156    "--namespaces",
157    default="namespaces.yaml",
158    type=click.File(),
159    help="Path to a yaml namespaces file",
160)
161@click.option(
162    "--app-listings-uri",
163    default="https://probeinfo.telemetry.mozilla.org/v2/glean/app-listings",
164    help="URI for probeinfo service v2 glean app listings",
165)
166@click.option(
167    "--target-dir",
168    default="looker-hub/",
169    type=click.Path(),
170    help="Path to a directory where lookml will be written",
171)
172@click.option(
173    "--metric-hub-repos",
174    "--metric-hub-repos",
175    multiple=True,
176    default=[METRIC_HUB_REPO, LOOKER_METRIC_HUB_REPO],
177    help="Repos to load metric configs from.",
178)
179@click.option(
180    "--only",
181    multiple=True,
182    default=[],
183    help="List of namespace names to generate lookml for.",
184)
185def lookml(namespaces, app_listings_uri, target_dir, metric_hub_repos, only):
186    """Generate lookml from namespaces."""
187    if metric_hub_repos:
188        MetricsConfigLoader.update_repos(metric_hub_repos)
189
190    glean_apps = _get_glean_apps(app_listings_uri)
191    return _lookml(namespaces, glean_apps, target_dir, only)
FILE_HEADER = '\n# *Do not manually modify this file*\n#\n# This file has been generated via https://github.com/mozilla/lookml-generator\n# You can extend this view in the looker-spoke-default project (https://github.com/mozilla/looker-spoke-default)\n\n'
lookml = <Command lookml>

Generate lookml from namespaces.