| # -*- coding: utf-8 -*- |
| # Copyright 2020 The Chromium OS Authors. All rights reserved. |
| # 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 xml.etree.ElementTree as ET |
| import textwrap as tw |
| 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)$' |
| 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(' <owner>{}</owner>'.format(o) |
| 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, 'raw-string metrics must be in a project with id type ' |
| "'none', but {} has id type '{}'".format( |
| project.name, 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) |