blob: dd12790170f1910af6ab37e581dd6cd6c0c92c5c [file] [log] [blame] [edit]
# -*- coding: utf-8 -*-
# Copyright 2020 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Model of a structured metrics description xml file.
This marshals an XML string into a Model, and validates that the XML is
semantically correct. The model can also be used to create a canonically
formatted version XML.
"""
import textwrap as tw
import xml.etree.ElementTree as ET
import model_util as util
def wrap(text, indent):
wrapper = tw.TextWrapper(
width=80, initial_indent=indent, subsequent_indent=indent
)
return wrapper.fill(tw.dedent(text))
class Model:
"""Represents all projects in the structured.xml file.
A Model is initialized with an XML string representing the top-level of
the structured.xml file. This file is built from three building blocks:
metrics, events, and projects. These have the following attributes.
METRIC
- summary
- data type
EVENT
- summary
- one or more metrics
PROJECT
- summary
- id specifier
- one or more owners
- one or more events
The following is an example input XML.
<structured-metrics>
<project name="MyProject">
<owner>owner@chromium.org</owner>
<id>none</id>
<summary> My project. </summary>
<event name="MyEvent">
<summary> My event. </summary>
<metric name="MyMetric" type="int">
<summary> My metric. </summary>
</metric>
</event>
</project>
</structured-metrics>
Calling str(model) will return a canonically formatted XML string.
"""
OWNER_REGEX = r"^.+@(chromium\.org|google\.com)$"
NAME_REGEX = r"^[A-Za-z0-9_.]+$"
TYPE_REGEX = r"^(hmac-string|raw-string|int|double)$"
ID_REGEX = r"^(none|per-project|uma)$"
def __init__(self, xml_string):
elem = ET.fromstring(xml_string)
util.check_attributes(elem, set())
util.check_children(elem, {"project"})
util.check_child_names_unique(elem, "project")
projects = util.get_compound_children(elem, "project")
self.projects = [Project(p) for p in projects]
def __repr__(self):
projects = "\n\n".join(str(p) for p in self.projects)
result = tw.dedent(
"""\
<structured-metrics>
{projects}
</structured-metrics>"""
)
return result.format(projects=projects)
class Project:
"""Represents a single structured metrics project.
A Project is initialized with an XML node representing one project, eg:
<project name="MyProject">
<owner>owner@chromium.org</owner>
<id>none</id>
<summary> My project. </summary>
<event name="MyEvent">
<summary> My event. </summary>
<metric name="MyMetric" type="int">
<summary> My metric. </summary>
</metric>
</event>
</project>
Calling str(project) will return a canonically formatted XML string.
"""
def __init__(self, elem):
util.check_attributes(elem, {"name"})
util.check_children(elem, {"id", "summary", "owner", "event"})
util.check_child_names_unique(elem, "event")
self.name = util.get_attr(elem, "name", Model.NAME_REGEX)
self.id = util.get_text_child(elem, "id", Model.ID_REGEX)
self.summary = util.get_text_child(elem, "summary")
self.owners = util.get_text_children(elem, "owner", Model.OWNER_REGEX)
self.events = [
Event(e, self) for e in util.get_compound_children(elem, "event")
]
def __repr__(self):
events = "\n\n".join(str(e) for e in self.events)
events = tw.indent(events, " ")
summary = wrap(self.summary, indent=" ")
owners = "\n".join(
f" <owner>{o}</owner>" for o in self.owners
)
result = tw.dedent(
"""\
<project name="{name}">
{owners}
<id>{id}</id>
<summary>
{summary}
</summary>
{events}
</project>"""
)
return result.format(
name=self.name,
owners=owners,
id=self.id,
summary=summary,
events=events,
)
class Event:
"""Represents a single structured metrics event.
An Event is initialized with an XML node representing one event, eg:
<event name="MyEvent">
<summary> My event. </summary>
<metric name="MyMetric" type="int">
<summary> My metric. </summary>
</metric>
</event>
Calling str(event) will return a canonically formatted XML string.
"""
def __init__(self, elem, project):
util.check_attributes(elem, {"name"})
util.check_children(elem, {"summary", "metric"})
util.check_child_names_unique(elem, "metric")
self.name = util.get_attr(elem, "name", Model.NAME_REGEX)
self.summary = util.get_text_child(elem, "summary")
self.metrics = [
Metric(m, project)
for m in util.get_compound_children(elem, "metric")
]
def __repr__(self):
metrics = "\n".join(str(m) for m in self.metrics)
metrics = tw.indent(metrics, " ")
summary = wrap(self.summary, indent=" ")
result = tw.dedent(
"""\
<event name="{name}">
<summary>
{summary}
</summary>
{metrics}
</events>"""
)
return result.format(name=self.name, summary=summary, metrics=metrics)
class Metric:
"""Represents a single metric.
A Metric is initialized with an XML node representing one metric, eg:
<metric name="MyMetric" type="int">
<summary> My metric. </summary>
</metric>
Calling str(metric) will return a canonically formatted XML string.
"""
def __init__(self, elem, project):
util.check_attributes(elem, {"name", "type"})
util.check_children(elem, {"summary"})
self.name = util.get_attr(elem, "name", Model.NAME_REGEX)
self.type = util.get_attr(elem, "type", Model.TYPE_REGEX)
self.summary = util.get_text_child(elem, "summary")
if self.type == "raw-string" and project.id != "none":
util.error(
elem,
f"raw-string metrics must be in a project with id type "
f"'none', but {project.name} has id type '{project.id}'",
)
def __repr__(self):
summary = wrap(self.summary, indent=" ")
result = tw.dedent(
"""\
<metric name="{name}" type="{type}">
<summary>
{summary}
</summary>
</metric>"""
)
return result.format(name=self.name, type=self.type, summary=summary)