Source code for msl.equipment.record_types

"""
Records from :ref:`equipment-database`\'s or :ref:`connections-database`\'s.
"""
from __future__ import annotations

import datetime
import json
from collections import OrderedDict
from collections.abc import Mapping
from enum import Enum
from xml.etree.ElementTree import Element

from .constants import Backend
from .constants import CR
from .constants import DataBits
from .constants import Interface
from .constants import LF
from .constants import Parity
from .constants import StopBits
from .factory import connect
from .factory import find_interface
from .utils import convert_to_date
from .utils import convert_to_enum


[docs] class RecordDict(Mapping): __slots__ = '_mapping' def __delattr__(self, item): # override to raise TypeError and to control the error message self._raise('item deletion') def __getattr__(self, item): return self._mapping[item] def __getitem__(self, item): return self._mapping[item] def __init__(self, dictionary): """A read-only dictionary that supports attribute access via a key lookup.""" if not isinstance(dictionary, dict): raise TypeError("Can only create a 'RecordDict' from a dict") # recursively make all values that are a dict a RecordDict for k, v in dictionary.items(): if isinstance(v, dict): dictionary[k] = RecordDict(v) if isinstance(v, (list, tuple)): def deep_tuple(a): return tuple(map(deep_tuple, a)) if isinstance(a, (list, tuple)) else a dictionary[k] = deep_tuple(v) super(RecordDict, self).__setattr__('_mapping', dictionary) def __iter__(self): return iter(self._mapping) def __len__(self): return len(self._mapping) def __repr__(self): return 'RecordDict<{}>'.format(self._mapping) def __setattr__(self, key, value): # override to raise TypeError and to control the error message self._raise('item assignment') def _raise(self, message): raise TypeError('A {!r} object does not support {}'.format(self.__class__.__name__, message)) def clear(self): self._raise('clearing')
[docs] def copy(self): """:class:`RecordDict`: Return a copy of the :class:`RecordDict`.""" return RecordDict(self._mapping.copy())
def fromkeys(self, *args, **kwargs): self._raise('fromkeys') def pop(self, *args, **kwargs): self._raise('popping') def popitem(self): self._raise('popitem') def setdefault(self, *args, **kwargs): self._raise('setdefault') def update(self, *args, **kwargs): self._raise('updating')
[docs] def to_xml(self, tag='RecordDict'): """Convert the :class:`RecordDict` to an XML :class:`~xml.etree.ElementTree.Element` Parameters ---------- tag : :class:`str` The name of the :class:`~xml.etree.ElementTree.Element`. Returns ------- :class:`~xml.etree.ElementTree.Element` The :class:`RecordDict` as an XML :class:`~xml.etree.ElementTree.Element`. """ root = Element(tag) for k, v in self._mapping.items(): if isinstance(v, RecordDict): element = v.to_xml(tag=k) else: element = Element(k) element.text = repr(v) root.append(element) return root
[docs] def to_json(self): """:class:`dict`: Convert the :class:`RecordDict` to be JSON_ serializable. .. _JSON: https://www.json.org/ """ root = dict() for k, v in self._mapping.items(): if isinstance(v, RecordDict): root[k] = v.to_json() elif isinstance(v, Enum): root[k] = v.name else: try: json.dumps(v) except TypeError: root[k] = str(v) # cannot be serialized else: root[k] = v # can be serialized return root
[docs] class Record(object):
[docs] def to_dict(self): """:class:`dict`: Convert the Record to a :class:`dict`.""" return dict((name, getattr(self, name)) for name in self.__slots__)
[docs] def to_json(self): """:class:`dict`: Convert the Record to be JSON_ serializable. This differs from :meth:`to_dict` such that all values that are not JSON_ serializable, like :class:`datetime.date` objects, are converted to a :class:`str`. .. _JSON: https://www.json.org/ """ raise NotImplementedError
[docs] def to_xml(self): """:class:`~xml.etree.ElementTree.Element`: Convert the Record to an XML :class:`~xml.etree.ElementTree.Element`.""" raise NotImplementedError
@staticmethod def _dict_to_str(dict_): if dict_: return '\n' + '\n'.join(' {}: {!r}'.format(k, v) for k, v in sorted(dict_.items())) else: return 'None' @staticmethod def _list_to_str(list_): if list_: return '\n' + '\n'.join([' {}'.format(line) for c in list_ for line in repr(c).splitlines()]) else: return 'None'
[docs] class EquipmentRecord(Record): __slots__ = ('alias', 'calibrations', 'category', 'connection', 'description', 'is_operable', 'maintenances', 'manufacturer', 'model', 'serial', 'team', 'unique_key', 'user_defined') def __init__(self, alias='', calibrations=None, category='', connection=None, description='', is_operable=False, maintenances=None, manufacturer='', model='', serial='', team='', unique_key='', **user_defined): """Contains the information about an equipment record in an :ref:`equipment-database`. Parameters ---------- alias : :class:`str` An alias to use to reference this equipment by. calibrations : :class:`list` of :class:`.CalibrationRecord` The calibration history of the equipment. category : :class:`str` The category (e.g., Laser, DMM) that the equipment belongs to. connection : :class:`.ConnectionRecord` The information necessary to communicate with the equipment. description : :class:`str` A description about the equipment. is_operable : :class:`bool` Whether the equipment is able to be used. maintenances : :class:`list` of :class:`.MaintenanceRecord` The maintenance history of the equipment. manufacturer : :class:`str` The name of the manufacturer of the equipment. model : :class:`str` The model number of the equipment. serial : :class:`str` The serial number (or unique identifier) of the equipment. team : :class:`str` The team (e.g., Light Standards) that the equipment belongs to. unique_key : :class:`str` The key that uniquely identifies the equipment record in a database. **user_defined All additional key-value pairs are added to the :attr:`.user_defined` attribute. """ self.alias = alias # the alias should be of type str, but this is up to the user """:class:`str`: An alias to use to reference this equipment by. The `alias` can be defined in 4 ways: * by specifying it when the EquipmentRecord is created * by setting the value after the EquipmentRecord has been created * in the **<equipment>** XML tag in a :ref:`configuration-file` * in the **Properties** field in a :ref:`connections-database` """ self.calibrations = self._set_calibrations(calibrations) """:class:`tuple` of :class:`.CalibrationRecord`: The calibration history of the equipment.""" self.category = '{}'.format(category) """:class:`str`: The category (e.g., Laser, DMM) that the equipment belongs to.""" self.description = '{}'.format(description) """:class:`str`: A description about the equipment.""" self.is_operable = bool(is_operable) """:class:`bool`: Whether the equipment is able to be used.""" self.maintenances = self._set_maintenances(maintenances) """:class:`tuple` of :class:`.MaintenanceRecord`: The maintenance history of the equipment.""" self.manufacturer = '{}'.format(manufacturer) """:class:`str`: The name of the manufacturer of the equipment.""" self.model = '{}'.format(model) """:class:`str`: The model number of the equipment.""" self.serial = '{}'.format(serial) """:class:`str`: The serial number (or unique identifier) of the equipment.""" # requires self.manufacturer, self.model and self.serial to be already defined self.connection = self._set_connection(connection) """:class:`.ConnectionRecord`: The information necessary to communicate with the equipment.""" # cache this value because __str__ is called a lot during logging self._str = 'EquipmentRecord<{}|{}|{}>'.format(self.manufacturer, self.model, self.serial) self.team = '{}'.format(team) """:class:`str`: The team (e.g., Light Standards) that the equipment belongs to.""" self.unique_key = '{}'.format(unique_key) """:class:`str`: The key that uniquely identifies the equipment record in a database.""" try: # a 'user_defined' kwarg was explicitly defined ud = user_defined.pop('user_defined') except KeyError: ud = user_defined else: ud.update(**user_defined) # the user_defined dict might still contain other key-value pairs self.user_defined = RecordDict(ud) """:class:`.RecordDict`: User-defined, key-value pairs.""" def __repr__(self): calibrations = self._list_to_str(self.calibrations) maintenances = self._list_to_str(self.maintenances) user_defined = self._dict_to_str(self.user_defined) if self.connection: connection = '\n ' + '\n '.join(repr(self.connection).splitlines()) else: connection = 'None' return 'EquipmentRecord\n' \ ' alias: {!r}\n' \ ' calibrations: {}\n' \ ' category: {!r}\n' \ ' connection: {}\n' \ ' description: {!r}\n' \ ' is_operable: {}\n' \ ' maintenances: {}\n' \ ' manufacturer: {!r}\n' \ ' model: {!r}\n' \ ' serial: {!r}\n' \ ' team: {!r}\n' \ ' unique_key: {!r}\n' \ ' user_defined: {}'.format(self.alias, calibrations, self.category, connection, self.description, self.is_operable, maintenances, self.manufacturer, self.model, self.serial, self.team, self.unique_key, user_defined) def __str__(self): return self._str def __setattr__(self, name, value): try: # once the `user_defined` attribute is created the class becomes read only # (except for the `alias` attribute which can be changed at any time) self.user_defined except AttributeError: super(EquipmentRecord, self).__setattr__(name, value) else: if name == 'alias': # only allow the alias to be modified super(EquipmentRecord, self).__setattr__(name, value) else: raise TypeError("An 'EquipmentRecord' cannot be modified. " "Cannot set {!r} to {!r}".format(name, value))
[docs] def connect(self, demo=None): """Establish a connection to the equipment. Calls the :func:`~msl.equipment.factory.connect` function. Parameters ---------- demo : :class:`bool`, optional Whether to simulate a connection to the equipment by opening a connection in demo mode. This allows you to test your code if the equipment is not physically connected to a computer. If :data:`None` then the `demo` value is determined from the :attr:`~.config.Config.DEMO_MODE` attribute. Returns ------- A :class:`~msl.equipment.connection.Connection` subclass. """ return connect(self, demo=demo)
[docs] def is_calibration_due(self, months: int = 0) -> bool: """Whether the equipment needs to be re-calibrated. :param months: The number of months to add to today's date to determine if the equipment needs to be re-calibrated within a certain amount of time. For example, if ``months = 6`` then that is a way of asking *"is a re-calibration due within the next 6 months?"*. :return: :data:`True` if the equipment needs to be re-calibrated, :data:`False` if it does not need to be re-calibrated (or it has never been calibrated). """ next_date = self.next_calibration_date() if next_date is None: return False cycle = max(0.0, months/12.0) ask_date = self._get_future_date(datetime.date.today(), cycle) return ask_date >= next_date
@property def latest_calibration(self): """:class:`.CalibrationRecord`: The latest calibration or :data:`None` if the equipment has never been calibrated.""" latest = None date = datetime.date(datetime.MINYEAR, 1, 1) for report in self.calibrations: # the calibration date gets precedence over the report date if report.calibration_date > date: date = report.calibration_date latest = report elif report.report_date > date: date = report.report_date latest = report return latest
[docs] def next_calibration_date(self) -> datetime.date | None: """The next calibration date or :data:`None` if the equipment has never been calibrated or if it is no longer in operation.""" if not self.is_operable: return report = self.latest_calibration if report is None or report.calibration_cycle <= 0: return # the calibration date gets precedence over the report date if report.calibration_date.year != datetime.MINYEAR: date = report.calibration_date elif report.report_date.year != datetime.MINYEAR: date = report.report_date else: return return self._get_future_date(date, report.calibration_cycle)
@staticmethod def _get_future_date(date: datetime.date, cycle: float) -> datetime.date: # cycle is in years year, month_decimal = divmod(cycle, 1) month = int(month_decimal * 12) # round down so a calibration happens sooner if date.month + month > 12: year += 1 month = date.month + month - 12 else: month = date.month + int(month) year = date.year + int(year) return date.replace(year=year, month=month)
[docs] def to_dict(self): """Convert this :class:`EquipmentRecord` to a :class:`dict`. Returns ------- :class:`dict` The :class:`EquipmentRecord` as a :class:`dict`. """ return { 'alias': self.alias, 'calibrations': tuple(cr.to_dict() for cr in self.calibrations), 'category': self.category, 'connection': None if self.connection is None else self.connection.to_dict(), 'description': self.description, 'is_operable': self.is_operable, 'maintenances': tuple(mh.to_dict() for mh in self.maintenances), 'manufacturer': self.manufacturer, 'model': self.model, 'serial': self.serial, 'team': self.team, 'unique_key': self.unique_key, 'user_defined': self.user_defined, }
[docs] def to_json(self): """Convert this :class:`EquipmentRecord` to be JSON_ serializable. .. _JSON: https://www.json.org/ Returns ------- :class:`dict` The :class:`EquipmentRecord` as a JSON_\\-serializable object. """ return { 'alias': self.alias, 'calibrations': tuple(cr.to_json() for cr in self.calibrations), 'category': self.category, 'connection': None if self.connection is None else self.connection.to_json(), 'description': self.description, 'is_operable': self.is_operable, 'maintenances': tuple(mh.to_json() for mh in self.maintenances), 'manufacturer': self.manufacturer, 'model': self.model, 'serial': self.serial, 'team': self.team, 'unique_key': self.unique_key, 'user_defined': self.user_defined.to_json(), }
[docs] def to_xml(self): """Convert this :class:`EquipmentRecord` to an XML :class:`~xml.etree.ElementTree.Element`. Returns ------- :class:`~xml.etree.ElementTree.Element` The :class:`EquipmentRecord` as an XML element. """ root = Element('EquipmentRecord') for name in EquipmentRecord.__slots__: element = Element(name) if name == 'connection': if self.connection is not None: element.append(self.connection.to_xml()) elif name == 'maintenances': for mh in self.maintenances: element.append(mh.to_xml()) elif name == 'calibrations': for cr in self.calibrations: element.append(cr.to_xml()) elif name == 'user_defined': for key, value in sorted(self.user_defined.items()): prop = Element(key) prop.text = '{}'.format(value) element.append(prop) else: element.text = '{}'.format(getattr(self, name)) root.append(element) return root
def _set_connection(self, record): if not record: return None if not isinstance(record, ConnectionRecord): if isinstance(record, dict): record = ConnectionRecord(**record) else: raise TypeError('Must pass in a ConnectionRecord object. Got {!r}'.format(record)) # ensure that the manufacturer, model and serial match for item in ('manufacturer', 'model', 'serial'): r, s = getattr(record, item), getattr(self, item) if not r: # then it was not set in the ConnectionRecord setattr(record, item, s) elif r != s: raise ValueError('ConnectionRecord.{0} ({1}) != EquipmentRecord.{0} ({2})'.format(item, r, s)) return record @staticmethod def _set_calibrations(calibrations): if calibrations is None: return tuple() reports = [] for report in calibrations: if isinstance(report, CalibrationRecord): reports.append(report) elif isinstance(report, dict): report['measurands'] = [MeasurandRecord(**m) for m in report['measurands']] reports.append(CalibrationRecord(**report)) else: raise TypeError("Invalid data type {!r} for creating a 'CalibrationRecord'".format(type(report))) return tuple(reports) @staticmethod def _set_maintenances(maintenances): if maintenances is None: return tuple() history = [] for maintenance in maintenances: if isinstance(maintenance, MaintenanceRecord): history.append(maintenance) elif isinstance(maintenance, dict): history.append(MaintenanceRecord(**maintenance)) else: raise TypeError("Invalid data type {!r} for creating a 'MaintenanceRecord'".format(type(maintenance))) return tuple(history)
[docs] class ConnectionRecord(Record): __slots__ = ('address', 'backend', 'interface', 'manufacturer', 'model', 'properties', 'serial') _LF = ['\\n', "'\\n'", '"\\n"', "b'\\n'", b'\n', b'\\n', b"b'\\n'"] _CR = ['\\r', "'\\r'", '"\\r"', "b'\\r'", b'\r', b'\\r', b"b'\\r'"] _CRLF = ['\\r\\n', "'\\r\\n'", '"\\r\\n"', "b'\\r\\n'", b'\r\n', b'\\r\\n', b"b'\r\n'", b"b'\\r\\n'"] def __init__(self, address='', backend=Backend.MSL, interface=None, manufacturer='', model='', serial='', **properties): """Contains the information about a connection record in a :ref:`connections-database`. Parameters ---------- address : :class:`str` The address to use for the connection (see :ref:`address-syntax` for examples). backend : :class:`str`, :class:`int`, or :class:`.Backend` The backend to use to communicate with the equipment. The value must be able to be converted to a :class:`.Backend` enum. interface : :class:`str`, :class:`int`, or :class:`.Interface` The interface to use to communicate with the equipment. If :data:`None` then determines the `interface` based on the value of `address`. If specified then the value must be able to be converted to a :class:`.Interface` enum. manufacturer : :class:`str` The name of the manufacturer of the equipment. model : :class:`str` The model number of the equipment. serial : :class:`str` The serial number (or unique identifier) of the equipment. properties Additional key-value pairs that are required to communicate with the equipment. """ self.address = '{}'.format(address) """:class:`str`: The address to use for the connection (see :ref:`address-syntax` for examples).""" self.backend = convert_to_enum(backend, Backend) """:class:`.Backend`: The backend to use to communicate with the equipment.""" self.interface = Interface.NONE """:class:`.Interface`: The interface that is used for the communication system that transfers data between a computer and the equipment (only used if the :attr:`.backend` is equal to :attr:`~.Backend.MSL`).""" if interface: self.interface = convert_to_enum(interface, Interface, to_upper=True) elif not address or self.backend != Backend.MSL: pass else: self.interface = find_interface(address) self.manufacturer = '{}'.format(manufacturer) """:class:`str`: The name of the manufacturer of the equipment.""" self.model = '{}'.format(model) """:class:`str`: The model number of the equipment.""" self.properties = self._set_properties(properties) """:class:`dict`: Additional key-value pairs that are required to communicate with the equipment. For example, communicating via RS-232 may require:: {'baud_rate': 19200, 'parity': 'even'} See the :ref:`connections-database` for examples on how to set the `properties`. """ self.serial = '{}'.format(serial) """:class:`str`: The serial number (or unique identifier) of the equipment.""" def __repr__(self): props = self._dict_to_str(dict((k, self.properties[k]) for k in sorted(self.properties))) return 'ConnectionRecord\n' \ ' address: {!r}\n' \ ' backend: {!r}\n' \ ' interface: {!r}\n' \ ' manufacturer: {!r}\n' \ ' model: {!r}\n' \ ' properties: {}\n' \ ' serial: {!r}'.format(self.address, self.backend, self.interface, self.manufacturer, self.model, props, self.serial) def __str__(self): return 'ConnectionRecord<{}|{}|{}>'.format(self.manufacturer, self.model, self.serial)
[docs] def to_json(self): """Convert this :class:`ConnectionRecord` to be JSON_ serializable. .. _JSON: https://www.json.org/ Returns ------- :class:`dict` The :class:`ConnectionRecord` as a JSON_\\-serializable object. """ props = dict() for k, v in self.properties.items(): if isinstance(v, Enum): props[k] = v.name else: try: json.dumps(v) except TypeError: props[k] = repr(v) # cannot be serialized else: props[k] = v # can be serialized return { 'address': self.address, 'backend': self.backend.name, 'interface': self.interface.name, 'manufacturer': self.manufacturer, 'model': self.model, 'properties': props, 'serial': self.serial, }
[docs] def to_xml(self): """Convert this :class:`ConnectionRecord` to an XML :class:`~xml.etree.ElementTree.Element`. Returns ------- :class:`~xml.etree.ElementTree.Element` The :class:`ConnectionRecord` as a XML :class:`~xml.etree.ElementTree.Element`. """ root = Element('ConnectionRecord') for name, value in self.to_dict().items(): element = Element(name) if name == 'properties': for prop_key in sorted(self.properties): prop_value = self.properties[prop_key] prop = Element(prop_key) if isinstance(prop_value, Enum): prop.text = prop_value.name elif prop_key.endswith('termination'): prop.text = repr(prop_value) elif isinstance(prop_value, bytes): prop.text = repr(prop_value) else: prop.text = '{}'.format(prop_value) element.append(prop) elif isinstance(value, Enum): element.text = value.name else: element.text = '{}'.format(value) root.append(element) return root
def _set_properties(self, kwargs): try: # a 'properties' kwarg was explicitly defined properties = kwargs.pop('properties') except KeyError: properties = kwargs else: if not properties: properties = {} elif not isinstance(properties, dict): raise TypeError('The properties kwarg for a ConnectionRecord must be of type dict. ' 'Got {!r} -> {!r}'.format(type(properties), properties)) properties.update(kwargs) if self.address.startswith('UDP'): properties['socket_type'] = 'SOCK_DGRAM' is_serial = self.interface == Interface.SERIAL if not is_serial and self.backend == Backend.PyVISA: for alias in ('COM', 'ASRL', 'ASRLCOM'): if self.address.startswith(alias): is_serial = True break for key, value in properties.items(): if is_serial: if key == 'parity': properties[key] = convert_to_enum(value, Parity, to_upper=True) elif key == 'stop_bits' or key == 'stopbits': properties[key] = convert_to_enum(value, StopBits, to_upper=True) elif key == 'data_bits' or key == 'bytesize': properties[key] = convert_to_enum(value, DataBits, to_upper=True) if key.endswith('termination'): if value in ConnectionRecord._CRLF: # must check before LR and CR checks properties[key] = CR + LF elif value in ConnectionRecord._LF: properties[key] = LF elif value in ConnectionRecord._CR: properties[key] = CR elif not isinstance(value, bytes) and value is not None: properties[key] = value.encode() return properties
[docs] class MaintenanceRecord(Record): __slots__ = ('comment', 'date') def __init__(self, comment='', date=None): """Contains the information about a maintenance record in an :ref:`equipment-database`. Parameters ---------- comment : :class:`str` A description of the maintenance that was performed. date : :class:`datetime.date`, :class:`datetime.datetime` or :class:`str` An object that can be converted to a :class:`datetime.date` object. If a :class:`str` then in the format ``'YYYY-MM-DD'``. """ self.comment = '{}'.format(comment) """:class:`str`: A description of the maintenance that was performed.""" self.date = convert_to_date(date) """:class:`datetime.date`: The date that the maintenance was performed.""" def __setattr__(self, name, value): try: self.date # once the `date` is defined the class becomes read only except AttributeError: super(MaintenanceRecord, self).__setattr__(name, value) else: raise TypeError("A 'MaintenanceRecord' cannot be modified. Cannot set {!r} to {!r}".format(name, value)) def __repr__(self): return 'MaintenanceRecord\n' \ ' comment: {!r}\n' \ ' date: {}'.format(self.comment, self.date) def __str__(self): return 'MaintenanceRecord<{}>'.format(self.date)
[docs] def to_json(self): """Convert this :class:`MaintenanceRecord` to be JSON_ serializable. .. _JSON: https://www.json.org/ Returns ------- :class:`dict` The :class:`MaintenanceRecord` as a JSON_\\-serializable object. """ return { 'comment': self.comment, 'date': self.date.isoformat(), }
[docs] def to_xml(self): """Convert this :class:`MaintenanceRecord` to an XML :class:`~xml.etree.ElementTree.Element`. Returns ------- :class:`~xml.etree.ElementTree.Element` The :class:`MaintenanceRecord` as a XML :class:`~xml.etree.ElementTree.Element`. """ root = Element('MaintenanceRecord') comment_element = Element('comment') comment_element.text = self.comment root.append(comment_element) date_element = Element('date') date_element.text = self.date.isoformat() date_element.attrib['format'] = 'YYYY-MM-DD' root.append(date_element) return root
[docs] class MeasurandRecord(Record): __slots__ = ('calibration', 'conditions', 'type', 'unit') def __init__(self, calibration=None, conditions=None, type='', unit=''): """Contains the information about a measurement for a calibration. Parameters ---------- calibration : :class:`dict` The information about the calibration. conditions : :class:`dict` The information about the conditions under which the measurement was performed. type : :class:`str` The type of measurement (e.g., voltage, temperature, transmittance, ...). unit : :class:`str` The unit that is associated with the measurement (e.g., V, deg C, %, ...). """ if calibration is None: calibration = {} elif not isinstance(calibration, dict): raise TypeError("the 'calibration' parameter must be a dict") if conditions is None: conditions = {} elif not isinstance(conditions, dict): raise TypeError("the 'conditions' parameter must be a dict") self.calibration = RecordDict(calibration) """:class:`.RecordDict`: The information about calibration.""" self.conditions = RecordDict(conditions) """:class:`.RecordDict`: The information about the measurement conditions.""" self.type = '{}'.format(type) """:class:`str`: The type of measurement (e.g., voltage, temperature, transmittance, ...).""" self.unit = '{}'.format(unit) """:class:`str`: The unit that is associated with the measurement (e.g., V, deg C, %, ...).""" def __setattr__(self, name, value): try: self.unit # once the `unit` is defined the class becomes read only except AttributeError: super(MeasurandRecord, self).__setattr__(name, value) else: raise TypeError("A 'MeasurandRecord' cannot be modified. Cannot set {!r} to {!r}".format(name, value)) def __repr__(self): cal = self._dict_to_str(self.calibration) con = self._dict_to_str(self.conditions) return 'MeasurandRecord\n' \ ' calibration: {}\n' \ ' conditions: {}\n' \ ' type: {!r}\n' \ ' unit: {!r}'.format(cal, con, self.type, self.unit) def __str__(self): return 'MeasurandRecord<{}>'.format(self.type)
[docs] def to_json(self): """Convert this :class:`MeasurandRecord` to be JSON_ serializable. .. _JSON: https://www.json.org/ Returns ------- :class:`dict` The :class:`MeasurandRecord` as a JSON_\\-serializable object. """ return { 'calibration': self.calibration.to_json(), 'conditions': self.conditions.to_json(), 'type': self.type, 'unit': self.unit, }
[docs] def to_xml(self): """Convert this :class:`MeasurandRecord` to an XML :class:`~xml.etree.ElementTree.Element`. Returns ------- :class:`~xml.etree.ElementTree.Element` The :class:`MeasurandRecord` as a XML :class:`~xml.etree.ElementTree.Element`. """ root = Element('MeasurandRecord') for name in ('calibration', 'conditions'): root.append(getattr(self, name).to_xml(tag=name)) for name in ('type', 'unit'): element = Element(name) element.text = getattr(self, name) root.append(element) return root
[docs] class CalibrationRecord(Record): __slots__ = ('calibration_cycle', 'calibration_date', 'measurands', 'report_date', 'report_number') def __init__(self, calibration_cycle=0, calibration_date=None, measurands=None, report_date=None, report_number=''): """Contains the information about a calibration record in an :ref:`equipment-database`. Parameters ---------- calibration_cycle : :class:`int` or :class:`float` The number of years that can pass before the equipment must be re-calibrated. calibration_date : :class:`datetime.date`, :class:`datetime.datetime` or :class:`str` The date that the calibration was performed. If a :class:`str` then in the format ``'YYYY-MM-DD'``. measurands : :class:`list` of :class:`.MeasurandRecord` The quantities that were measured. report_date : :class:`datetime.date`, :class:`datetime.datetime` or :class:`str` The date that the report was issued. If a :class:`str` then in the format ``'YYYY-MM-DD'``. report_number : :class:`str` The report number. """ if measurands is None: measurands = [] measures = [] for m in measurands: if isinstance(m, MeasurandRecord): measures.append(m) elif m and isinstance(m, dict): measures.append(MeasurandRecord(**m)) self.calibration_cycle = float(calibration_cycle) """:class:`float`: The number of years that can pass before the equipment must be re-calibrated.""" self.calibration_date = convert_to_date(calibration_date) """:class:`datetime.date`: The date that the calibration was performed.""" self.measurands = RecordDict(OrderedDict((m.type, m) for m in measures)) """:class:`.RecordDict`: The quantities that were measured.""" self.report_date = convert_to_date(report_date) """:class:`datetime.date`: The date that the report was issued.""" self.report_number = '{}'.format(report_number) """:class:`str`: The report number.""" def __setattr__(self, name, value): try: self.report_number # once the `report_number` is defined the class becomes read only except AttributeError: super(CalibrationRecord, self).__setattr__(name, value) else: raise TypeError("A 'CalibrationRecord' cannot be modified. Cannot set {!r} to {!r}".format(name, value)) def __repr__(self): if self.measurands: measurands = '\n' + '\n'.join(' {}'.format(line) for value in self.measurands.values() for line in repr(value).splitlines()) else: measurands = 'None' return 'CalibrationRecord\n' \ ' calibration_cycle: {}\n' \ ' calibration_date: {}\n' \ ' measurands: {}\n' \ ' report_date: {}\n' \ ' report_number: {!r}'.format(self.calibration_cycle, self.calibration_date, measurands, self.report_date, self.report_number) def __str__(self): return 'CalibrationRecord<{}>'.format(self.report_number)
[docs] def to_json(self): """Convert this :class:`CalibrationRecord` to be JSON_ serializable. .. _JSON: https://www.json.org/ Returns ------- :class:`dict` The :class:`CalibrationRecord` as a JSON_\\-serializable object. """ return { 'calibration_cycle': self.calibration_cycle, 'calibration_date': self.calibration_date.isoformat(), 'measurands': tuple(m.to_json() for m in self.measurands.values()), 'report_date': self.report_date.isoformat(), 'report_number': self.report_number }
[docs] def to_xml(self): """Convert this :class:`CalibrationRecord` to an XML :class:`~xml.etree.ElementTree.Element`. Returns ------- :class:`~xml.etree.ElementTree.Element` The :class:`CalibrationRecord` as a XML :class:`~xml.etree.ElementTree.Element`. """ root = Element('CalibrationRecord') calibration_date = Element('calibration_date') calibration_date.text = self.calibration_date.isoformat() calibration_date.attrib['format'] = 'YYYY-MM-DD' root.append(calibration_date) calibration_cycle = Element('calibration_cycle') calibration_cycle.text = str(self.calibration_cycle) calibration_cycle.attrib['unit'] = 'years' root.append(calibration_cycle) measurands = Element('measurands') for measurand in self.measurands.values(): measurands.append(measurand.to_xml()) root.append(measurands) report_number = Element('report_number') report_number.text = self.report_number root.append(report_number) report_date = Element('report_date') report_date.text = self.report_date.isoformat() report_date.attrib['format'] = 'YYYY-MM-DD' root.append(report_date) return root