"""
Records from :ref:`equipment-database`\'s or :ref:`connections-database`\'s.
"""
from __future__ import unicode_literals
import json
import datetime
from enum import Enum
from xml.etree.cElementTree import Element
from collections import OrderedDict
try:
from collections.abc import Mapping
except ImportError:
from collections import Mapping # Python 2.7
from dateutil.relativedelta import relativedelta
from .utils import (
convert_to_enum,
convert_to_date,
)
from .constants import (
Parity,
StopBits,
DataBits,
Backend,
Interface,
LF,
CR,
)
from .factory import (
connect,
find_interface,
)
[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=0):
"""Whether the equipment needs to be re-calibrated.
Parameters
----------
months : :class:`int`, optional
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?"*.
Returns
-------
:class:`bool`
: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
ask_date = datetime.date.today() + relativedelta(months=max(0, int(months)))
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):
"""The date that the next calibration is due.
Returns
-------
:class:`datetime.date`
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 None
report = self.latest_calibration
if report is None or report.calibration_cycle <= 0:
return None
# 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 None
years = int(report.calibration_cycle)
months = int(round(12 * (report.calibration_cycle - years)))
return date + relativedelta(years=years, months=months)
[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