Source code for glean_parser.markdown

# -*- coding: utf-8 -*-

# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

"""
Outputter to generate Markdown documentation for metrics.
"""

from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Union
from urllib.parse import urlsplit, parse_qs


from . import __version__
from . import metrics
from . import pings
from . import util
from collections import defaultdict


[docs] def extra_info(obj: Union[metrics.Metric, pings.Ping]) -> List[Tuple[str, str]]: """ Returns a list of string to string tuples with extra information for the type (e.g. extra keys for events) or an empty list if nothing is available. """ extra_info = [] if isinstance(obj, metrics.Event): for key in obj.allowed_extra_keys: extra_info.append((key, obj.extra_keys[key]["description"])) if isinstance(obj, metrics.Labeled) and obj.ordered_labels is not None: for label in obj.ordered_labels: extra_info.append((label, None)) if isinstance(obj, metrics.Quantity): extra_info.append(("unit", obj.unit)) return extra_info
[docs] def ping_desc( ping_name: str, custom_pings_cache: Optional[Dict[str, pings.Ping]] = None ) -> str: """ Return a text description of the ping. If a custom_pings_cache is available, look in there for non-reserved ping names description. """ desc = "" if ping_name in pings.RESERVED_PING_NAMES: desc = ( "This is a built-in ping that is assembled out of the " "box by the Glean SDK." ) elif ping_name == "all-pings": desc = "These metrics are sent in every ping." elif custom_pings_cache is not None and ping_name in custom_pings_cache: desc = custom_pings_cache[ping_name].description return desc
[docs] def metrics_docs(obj_name: str) -> str: """ Return a link to the documentation entry for the Glean SDK metric of the requested type. """ # We need to fixup labeled stuff, as types are singular and docs refer # to them as plural. fixedup_name = obj_name if obj_name.startswith("labeled_"): fixedup_name += "s" return f"https://mozilla.github.io/glean/book/user/metrics/{fixedup_name}.html"
[docs] def ping_docs(ping_name: str) -> str: """ Return a link to the documentation entry for the requested Glean SDK built-in ping. """ if ping_name not in pings.RESERVED_PING_NAMES: return "" return f"https://mozilla.github.io/glean/book/user/pings/{ping_name}.html"
[docs] def if_empty( ping_name: str, custom_pings_cache: Optional[Dict[str, pings.Ping]] = None ) -> bool: if custom_pings_cache is not None and ping_name in custom_pings_cache: return custom_pings_cache[ping_name].send_if_empty else: return False
[docs] def ping_reasons( ping_name: str, custom_pings_cache: Dict[str, pings.Ping] ) -> Dict[str, str]: """ Returns the reasons dictionary for the ping. """ if ping_name == "all-pings": return {} elif ping_name in custom_pings_cache: return custom_pings_cache[ping_name].reasons return {}
[docs] def ping_data_reviews( ping_name: str, custom_pings_cache: Optional[Dict[str, pings.Ping]] = None ) -> Optional[List[str]]: if custom_pings_cache is not None and ping_name in custom_pings_cache: return custom_pings_cache[ping_name].data_reviews else: return None
[docs] def ping_review_title(data_url: str, index: int) -> str: """ Return a title for a data review in human readable form. :param data_url: A url for data review. :param index: Position of the data review on list (e.g: 1, 2, 3...). """ url_object = urlsplit(data_url) # Bugzilla urls like `https://bugzilla.mozilla.org/show_bug.cgi?id=1581647` query = url_object.query params = parse_qs(query) # GitHub urls like `https://github.com/mozilla-mobile/fenix/pull/1707` path = url_object.path short_url = path[1:].replace("/pull/", "#") if params and params["id"]: return f"Bug {params['id'][0]}" elif url_object.netloc == "github.com": return short_url return f"Review {index}"
[docs] def ping_bugs( ping_name: str, custom_pings_cache: Optional[Dict[str, pings.Ping]] = None ) -> Optional[List[str]]: if custom_pings_cache is not None and ping_name in custom_pings_cache: return custom_pings_cache[ping_name].bugs else: return None
[docs] def ping_include_client_id( ping_name: str, custom_pings_cache: Optional[Dict[str, pings.Ping]] = None ) -> bool: if custom_pings_cache is not None and ping_name in custom_pings_cache: return custom_pings_cache[ping_name].include_client_id else: return False
[docs] def data_sensitivity_numbers( data_sensitivity: Optional[List[metrics.DataSensitivity]], ) -> str: if data_sensitivity is None: return "unknown" else: return ", ".join(str(x.value) for x in data_sensitivity)
[docs] def output_markdown( objs: metrics.ObjectTree, output_dir: Path, options: Optional[Dict[str, Any]] = None ) -> None: """ Given a tree of objects, output Markdown docs to `output_dir`. This produces a single `metrics.md`. The file contains a table of contents and a section for each ping metrics are collected for. :param objects: A tree of objects (metrics and pings) as returned from `parser.parse_objects`. :param output_dir: Path to an output directory to write to. :param options: options dictionary, with the following optional key: - `project_title`: The projects title. """ if options is None: options = {} # Build a dictionary that associates pings with their metrics. # # { # "baseline": [ # { ... metric data ... }, # ... # ], # "metrics": [ # { ... metric data ... }, # ... # ], # ... # } # # This also builds a dictionary of custom pings, if available. custom_pings_cache: Dict[str, pings.Ping] = defaultdict() metrics_by_pings: Dict[str, List[metrics.Metric]] = defaultdict(list) for _category_key, category_val in objs.items(): for obj in category_val.values(): # Filter out custom pings. We will need them for extracting # the description if isinstance(obj, pings.Ping): custom_pings_cache[obj.name] = obj # Pings that have `send_if_empty` set to true, # might not have any metrics. They need to at least have an # empty array of metrics to show up on the template. if obj.send_if_empty and not metrics_by_pings[obj.name]: metrics_by_pings[obj.name] = [] # If this is an internal Glean metric, and we don't # want docs for it. if isinstance(obj, metrics.Metric) and not obj.is_internal_metric(): # If we get here, obj is definitely a metric we want # docs for. for ping_name in obj.send_in_pings: metrics_by_pings[ping_name].append(obj) # Sort the metrics by their identifier, to make them show up nicely # in the docs and to make generated docs reproducible. for ping_name in metrics_by_pings: metrics_by_pings[ping_name] = sorted( metrics_by_pings[ping_name], key=lambda x: x.identifier() ) project_title = options.get("project_title", "this project") introduction_extra = options.get("introduction_extra") template = util.get_jinja2_template( "markdown.jinja2", filters=( ("extra_info", extra_info), ("metrics_docs", metrics_docs), ("ping_desc", lambda x: ping_desc(x, custom_pings_cache)), ("ping_send_if_empty", lambda x: if_empty(x, custom_pings_cache)), ("ping_docs", ping_docs), ("ping_reasons", lambda x: ping_reasons(x, custom_pings_cache)), ("ping_data_reviews", lambda x: ping_data_reviews(x, custom_pings_cache)), ("ping_review_title", ping_review_title), ("ping_bugs", lambda x: ping_bugs(x, custom_pings_cache)), ( "ping_include_client_id", lambda x: ping_include_client_id(x, custom_pings_cache), ), ("data_sensitivity_numbers", data_sensitivity_numbers), ), ) filename = "metrics.md" filepath = output_dir / filename with filepath.open("w", encoding="utf-8") as fd: fd.write( template.render( parser_version=__version__, metrics_by_pings=metrics_by_pings, project_title=project_title, introduction_extra=introduction_extra, ) ) # Jinja2 squashes the final newline, so we explicitly add it fd.write("\n")