# -*- coding: utf-8 -*- # Copyright 2021 The Chromium 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)) # TODO(crbug.com/1148168): This can be removed and replaced with textwrap.indent # once this is run under python3. def indent(text, prefix): return '\n'.join(prefix + line if line else '' for line in text.split('\n')) 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. owner@chromium.org none profile My project. My event. My metric. 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)$' SCOPE_REGEX = r'^(profile|device)$' 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("""\ {projects} """) 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: owner@chromium.org none project My project. My event. My metric. Calling str(project) will return a canonically formatted XML string. """ def __init__(self, elem): util.check_attributes(elem, {'name'}) util.check_children(elem, {'id', 'scope', '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.scope = util.get_text_child(elem, 'scope', Model.SCOPE_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 = indent(events, ' ') summary = wrap(self.summary, indent=' ') owners = '\n'.join(' {}'.format(o) for o in self.owners) result = tw.dedent("""\ {owners} {id} {scope} {summary} {events} """) return result.format(name=self.name, owners=owners, id=self.id, scope=self.scope, summary=summary, events=events) class Event: """Represents a single structured metrics event. An Event is initialized with an XML node representing one event, eg: My event. My metric. 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 = indent(metrics, ' ') summary = wrap(self.summary, indent=' ') result = tw.dedent("""\ {summary} {metrics} """) 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: My 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("""\ {summary} """) return result.format(name=self.name, type=self.type, summary=summary)