"""
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