Source code for lsst.verify.jobmetadata

#
# LSST Data Management System
#
# This product includes software developed by the
# LSST Project (http://www.lsst.org/).
#
# See COPYRIGHT file at the top of the source tree.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the LSST License Statement and
# the GNU General Public License along with this program.  If not,
# see <https://www.lsstcorp.org/LegalNotices/>.
#
from __future__ import print_function

__all__ = ['Metadata']

# Get ChainMap backport
from future.standard_library import install_aliases
install_aliases()  # noqa: E402

try:
    from collections import ChainMap
except ImportError:
    # future 0.16.0 doesn't do the import right; this will be fixed in 0.16.1
    # https://github.com/PythonCharmers/python-future/issues/226
    from future.backports.misc import ChainMap
import json
import re

from .jsonmixin import JsonSerializationMixin


[docs]class Metadata(JsonSerializationMixin): """Container for verification framework job metadata. Metadata are key-value terms. Both keys and values should be JSON-serializable. Parameters ---------- measurement_set : `lsst.verify.MeasurementSet`, optional When provided, metadata with keys prefixed by metric names are deferred to `Metadata` instances attached to measurements (`lsst.verify.Measurement.notes`). data : `dict`, optional Dictionary to seed metadata. """ # Pattern for detecting metric name prefixes in names _prefix_pattern = re.compile('^(\S+\.\S+)\.') def __init__(self, measurement_set, data=None): # Dict of job metadata not stored with a mesaurement self._data = {} # Measurement set to get measurement annotations from self._meas_set = measurement_set # Initialize the ChainMap. The first item in the chain map is the # Metadata object's own _data. This is generic metadata. Additional # items in the chain are Measurement.notes annotations for all # measurements in the measurement_set. self._chain = ChainMap(self._data) self._cached_prefixes = set() self._refresh_chainmap() if data is not None: self.update(data) def _refresh_chainmap(self): prefixes = set([str(name) for name in self._meas_set]) if self._cached_prefixes != prefixes: self._cached_prefixes = prefixes self._chain = ChainMap(self._data) for _, measurement in self._meas_set.items(): # Get the dict instance directly so we don't use # the MeasurementNotes's key auto-prefixing. self._chain.maps.append(measurement.notes._data) @staticmethod def _get_prefix(key): """Get the prefix of a measurement not, if it exists. Examples -------- >>> Metadata._get_prefix('note') is None True >>> Metadata._get_prefix('validate_drp.PA1.note') 'validate_drp.PA1.' To get the metric name: >>> prefix = Metadata._get_prefix('validate_drp.PA1.note') >>> prefix.rstrip('.') 'validate_drp.PA1' """ match = Metadata._prefix_pattern.match(key) if match is not None: return match.group(0) else: return None def __getitem__(self, key): self._refresh_chainmap() return self._chain[key] def __setitem__(self, key, value): prefix = Metadata._get_prefix(key) if prefix is not None: metric_name = prefix.rstrip('.') if metric_name in self._meas_set: # turn prefix into a metric name self._meas_set[metric_name].notes[key] = value return # No matching measurement; insert into general metadata self._data[key] = value def __delitem__(self, key): prefix = Metadata._get_prefix(key) if prefix is not None: metric_name = prefix.rstrip('.') if metric_name in self._meas_set: del self._meas_set[metric_name].notes[key] return # No matching measurement; delete from general metadata del self._data[key] def __contains__(self, key): self._refresh_chainmap() return key in self._chain def __len__(self): self._refresh_chainmap() return len(self._chain) def __iter__(self): self._refresh_chainmap() for key in self._chain: yield key def __eq__(self, other): # No explicit chain refresh because __len__ already does it if len(self) != len(other): return False for key, value in other.items(): if key not in self: return False if value != self[key]: return False return True def __ne__(self, other): return not self.__eq__(other) def __str__(self): json_data = self.json return json.dumps(json_data, sort_keys=True, indent=4) def __repr__(self): return repr(self._chain) def _repr_html_(self): return self.__str__()
[docs] def keys(self): """Get a `list` of metadata keys. Returns ------- keys : `list` of `str` These keys keys can be used to access metadata values (like a `dict`). """ return [key for key in self]
[docs] def items(self): """Iterate over key-value metadata pairs. Yields ------ item : `tuple` A metadata item is a tuple of: - Key (`str`). - Value (object). """ self._refresh_chainmap() for item in self._chain.items(): yield item
[docs] def update(self, data): """Update metadata with key-value pairs from a `dict`-like object. Parameters ---------- data : `dict`-like The ``data`` object needs to provide an ``items`` method to iterate over its key-value pairs. If this ``Metadata`` instance already has a key, the value will be overwritten with the value from ``data``. """ for key, value in data.items(): self[key] = value
@property def json(self): """A `dict` that can be serialized as semantic SQUASH JSON. Keys in the `dict` are metadata keys (see `Metadata.keys`). Values are the associated metadata values as JSON-serializable objects. """ self._refresh_chainmap() return self.jsonify_dict(self._chain)