Skip to content

Register¤

An equipment register can be stored in a single XML file or distributed across multiple XML files. In this example we load a single XML file.

>>> from msl.equipment import Register
>>> register = Register("tests/data/mass/register.xml")

Printing the register object shows the name of the team that is responsible for the equipment register and the number of Equipment items that are in the register.

>>> register
Register(team='Mass' (2 equipment))
>>> register.team
'Mass'

A register behaves like a sequence of Equipment items. You can get the number of items in the sequence and iterate over the sequence,

>>> len(register)
2
>>> for equipment in register:
...     print(equipment.id)
...
MSLE.M.001
MSLE.M.092

access equipment by its index, its equipment id or its alias.

>>> register[1].id  # index
'MSLE.M.092'
>>> register["MSLE.M.092"].id  # equipment id
'MSLE.M.092'
>>> register["Bob"].id  # alias
'MSLE.M.092'

The get method will attempt to get the Equipment at the specified index, id or alias, but will return None (instead of raising an exception) if the Equipment cannot be found in the register.

>>> assert register.get(4) is None  # invalid index
>>> assert register.get("MSLE.M.999") is None  # invalid equipment id
>>> register.get("Bob")  # valid alias
Equipment(id='MSLE.M.092', manufacturer='XYZ', model='A', serial='b' (4 reports))

The find method searches the register to find equipment that contain certain text. You may use a regular-expression pattern to find matching equipment items. Here we find all equipment that have the text Hygrometer in one of the Equipment attribute values that are considered in the search.

>>> for hygrometer in register.find("Hygrometer"):
...    print(hygrometer)
Equipment(id='MSLE.M.092', manufacturer='XYZ', model='A', serial='b' (4 reports))

We see that one equipment was found and that there are four calibration reports associated with the equipment. From the Equipment instance, we can get the latest calibration report for a certain Component name and Measurand quantity.

Tip

If the equipment contains only one Measurand and one Component you do not need to specify the name and quantity keyword arguments.

>>> report = hygrometer.latest_report(name="Probe 1", quantity="Humidity")
>>> report
LatestReport(name='Probe 1', quantity='Humidity', id='Humidity/2023/583' (1 equation))

We see that the calibration report contains one Equation. We can use the equation to apply a correction to measured values and to calculate the uncertainty.

>>> value = report.equation.value
>>> value.equation
'R - 7.131e-2 - 3.951e-2*R + 3.412e-4*pow(R,2) + 2.465e-3*t + 1.034e-3*R*t - 5.297e-6*pow(R,2)*t'
>>> value(R=[45.5, 46.1], t=[20.1, 20.0])
array([45.1121266, 45.7099039])
>>> report.equation.uncertainty(R=[45.5, 46.1], t=[20.1, 20.0])
array([0.355, 0.355])
>>> report.equation.unit
'%rh'

From the LatestReport instance, we can, for example, get the date when the next calibration is due.

>>> report.next_calibration_date
datetime.date(2028, 8, 14)

Register ¤

Register(*sources: XMLSource | Element[str])

Represents the register element in an equipment register.

Specifying multiple sources allows for storing an equipment register across multiple files for the same team. Not specifying a source creates a new (empty) register.

Parameters:

Name Type Description Default
sources XMLSource | Element[str]

The path-like, file-like or Element objects that represent an equipment register.

()
Source code in src/msl/equipment/schema.py
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
def __init__(self, *sources: XMLSource | Element[str]) -> None:
    """Represents the [register][element_register] element in an equipment register.

    Specifying multiple sources allows for storing an equipment register across multiple
    files for the same team. Not specifying a source creates a new (empty) register.

    Args:
        sources: The [path-like][path-like object], [file-like][file-like object] or
            [Element][xml.etree.ElementTree.Element] objects that represent an equipment register.
    """
    team = ""
    self._elements: list[Element[str]] = []
    for source in sources:
        root = source if isinstance(source, Element) else ElementTree().parse(source)
        t = root.attrib.get("team", "")
        if not team:
            team = t

        if team != t:
            msg = f"Cannot merge equipment registers from different teams, {team!r} != {t!r}"
            raise ValueError(msg)

        self._elements.extend(root)

    self._team: str = team
    self._equipment: list[Equipment | None] = [None] * len(self._elements)

    # a mapping between the alias/id and the index number in the register
    self._index_map: dict[str, int] = {str(e[0].text): i for i, e in enumerate(self._elements)}  # e[0] is the ID
    self._index_map.update({e.attrib["alias"]: i for i, e in enumerate(self._elements) if e.attrib.get("alias")})

NAMESPACE class-attribute instance-attribute ¤

NAMESPACE: str = 'https://measurement.govt.nz/equipment-register'

Default XML namespace.

team property writable ¤

team: str

str — The name of the team that is responsible for the equipment register.

add ¤

add(equipment: Equipment) -> None

Add equipment to the register.

Parameters:

Name Type Description Default
equipment Equipment

The equipment to add.

required
Source code in src/msl/equipment/schema.py
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
def add(self, equipment: Equipment) -> None:
    """Add equipment to the register.

    Args:
        equipment: The equipment to add.
    """
    if equipment.id:
        self._index_map[equipment.id] = len(self._equipment)
    if equipment.alias:
        self._index_map[equipment.alias] = len(self._equipment)
    self._equipment.append(equipment)

find ¤

find(pattern: str | Pattern[str], *, flags: int = 0) -> Iterator[Equipment]

Find equipment in the register.

The following values are used in the search:

Parameters:

Name Type Description Default
pattern str | Pattern[str]

A regular-expression pattern to use to find equipment.

required
flags int

The flags to use to compile the pattern. See re.compile for more details.

0

Yields:

Type Description
Equipment

Equipment that match the pattern.

Source code in src/msl/equipment/schema.py
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
2920
2921
2922
2923
2924
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
def find(self, pattern: str | re.Pattern[str], *, flags: int = 0) -> Iterator[Equipment]:  # noqa: C901
    """Find equipment in the register.

    The following values are used in the search:

    * keywords: [Equipment][msl.equipment.schema.Equipment]
    * description: [Equipment][msl.equipment.schema.Equipment]
    * manufacturer: [Equipment][msl.equipment.schema.Equipment]
    * model: [Equipment][msl.equipment.schema.Equipment]
    * serial: [Equipment][msl.equipment.schema.Equipment]
    * id: [Equipment][msl.equipment.schema.Equipment], [Report][msl.equipment.schema.Report], [DigitalReport][msl.equipment.schema.DigitalReport]
    * location: [Equipment][msl.equipment.schema.Equipment]
    * quantity: [Measurand][msl.equipment.schema.Measurand]
    * name: [Component][msl.equipment.schema.Component]
    * entered_by: [Equipment][msl.equipment.schema.Equipment], [PerformanceCheck][msl.equipment.schema.PerformanceCheck], [Report][msl.equipment.schema.Report]
    * checked_by: [Equipment][msl.equipment.schema.Equipment], [PerformanceCheck][msl.equipment.schema.PerformanceCheck], [Report][msl.equipment.schema.Report]
    * performed_by: [Alteration][msl.equipment.schema.Alteration], [CompletedTask][msl.equipment.schema.CompletedTask], [PlannedTask][msl.equipment.schema.PlannedTask]
    * comment: [CVDEquation][msl.equipment.schema.CVDEquation], [Equation][msl.equipment.schema.Equation], [File][msl.equipment.schema.File], [Table][msl.equipment.schema.Table], [Deserialised][msl.equipment.schema.Deserialised], [DigitalReport][msl.equipment.schema.DigitalReport]
    * format: [DigitalReport][msl.equipment.schema.DigitalReport]
    * details: [Alteration][msl.equipment.schema.Alteration], [Adjustment][msl.equipment.schema.Adjustment]
    * task: [CompletedTask][msl.equipment.schema.CompletedTask], [PlannedTask][msl.equipment.schema.PlannedTask]
    * asset_number: [CapitalExpenditure][msl.equipment.schema.CapitalExpenditure]
    * service_agent: [QualityManual][msl.equipment.schema.QualityManual]
    * technical_procedures: [QualityManual][msl.equipment.schema.QualityManual]

    Args:
        pattern: A [regular-expression pattern](https://regexr.com/) to use to find equipment.
        flags: The flags to use to compile the `pattern`. See [re.compile][] for more details.

    Yields:
        Equipment that match the `pattern`.
    """  # noqa: E501

    def comment_search(item: Report | PerformanceCheck) -> bool:
        for cvd_equation in item.cvd_equations:
            if regex.search(cvd_equation.comment) is not None:
                return True
        for equation in item.equations:
            if regex.search(equation.comment) is not None:
                return True
        for file in item.files:
            if regex.search(file.comment) is not None:
                return True
        for table in item.tables:
            if regex.search(table.comment) is not None:
                return True
        return any(regex.search(deserialised.comment) is not None for deserialised in item.deserialisers)

    def task_search(m: Maintenance) -> bool:
        for c in m.completed:
            if regex.search(c.task) is not None:
                return True
            if regex.search(c.performed_by) is not None:
                return True
        for p in m.planned:
            if regex.search(p.task) is not None:
                return True
            if regex.search(p.performed_by) is not None:
                return True
        return False

    def alteration_search(alterations: tuple[Alteration, ...]) -> bool:
        for a in alterations:
            if regex.search(a.details) is not None:
                return True
            if regex.search(a.performed_by) is not None:
                return True
        return False

    def calibrations_search(e: Equipment) -> bool:  # noqa: C901, PLR0911, PLR0912
        for m in e.calibrations:
            if regex.search(m.quantity) is not None:
                return True
            for c in m.components:
                if regex.search(c.name) is not None:
                    return True
                for r in c.reports:
                    if regex.search(r.entered_by) is not None:
                        return True
                    if regex.search(r.checked_by) is not None:
                        return True
                    if comment_search(r):
                        return True
                    if regex.search(r.id) is not None:
                        return True
                for pc in c.performance_checks:
                    if regex.search(pc.entered_by) is not None:
                        return True
                    if regex.search(pc.checked_by) is not None:
                        return True
                    if comment_search(pc):
                        return True
                for a in c.adjustments:
                    if regex.search(a.details) is not None:
                        return True
                for dr in c.digital_reports:
                    if regex.search(dr.format.value) is not None:
                        return True
                    if regex.search(dr.id) is not None:
                        return True
                    if regex.search(dr.comment) is not None:
                        return True
        return False

    def asset_number_search(f: Financial) -> bool:
        if f.capital_expenditure is None:
            return False
        return regex.search(f.capital_expenditure.asset_number) is not None

    regex = re.compile(pattern, flags=flags)
    for equipment in self:
        if (
            regex.search(" ".join(equipment.keywords)) is not None
            or regex.search(equipment.description) is not None
            or regex.search(equipment.manufacturer) is not None
            or regex.search(equipment.model) is not None
            or regex.search(equipment.serial) is not None
            or regex.search(equipment.id) is not None
            or regex.search(equipment.location) is not None
            or regex.search(equipment.entered_by) is not None
            or regex.search(equipment.checked_by) is not None
            or calibrations_search(equipment)
            or alteration_search(equipment.alterations)
            or task_search(equipment.maintenance)
            or asset_number_search(equipment.quality_manual.financial)
            or regex.search(equipment.quality_manual.service_agent) is not None
            or regex.search(" ".join(equipment.quality_manual.technical_procedures)) is not None
        ):
            yield equipment

get ¤

get(item: int | str) -> Equipment | None

Get an Equipment item from the register.

This method will ignore all errors if the register does not contain the requested Equipment item.

Tip

You can also treat a register instance as a sequence of Equipment items.

Using the indexable notation on a register instance to access an Equipment item by using the alias of the equipment or the index within the register could raise an exception

>>> register["unknown-alias"]
Traceback (most recent call last):
...
ValueError: No equipment exists with the alias or id 'unknown-alias'

>>> register[243]
Traceback (most recent call last):
...
IndexError: list index out of range

whereas these errors can be silenced by using the get method

>>> assert register.get("unknown") is None
>>> assert register.get(243) is None

Parameters:

Name Type Description Default
item int | str

The index number, equipment id value or the equipment alias value in the register.

required

Returns:

Type Description
Equipment | None

The Equipment item if item is valid, otherwise None.

Source code in src/msl/equipment/schema.py
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
2977
2978
2979
2980
2981
2982
2983
2984
2985
2986
2987
2988
2989
2990
def get(self, item: int | str) -> Equipment | None:
    """Get an [Equipment][msl.equipment.schema.Equipment] item from the register.

    This method will ignore all errors if the register does not contain the requested
    [Equipment][msl.equipment.schema.Equipment] item.

    !!! tip
        You can also treat a _register_ instance as a sequence of [Equipment][msl.equipment.schema.Equipment] items.

    <!--
    >>> from msl.equipment import Register
    >>> register = Register("tests/data/mass/register.xml")

    -->

    Using the _indexable_ notation on a _register_ instance to access an [Equipment][msl.equipment.schema.Equipment]
    item by using the alias of the equipment or the index within the register could raise an exception

    ```pycon
    >>> register["unknown-alias"]
    Traceback (most recent call last):
    ...
    ValueError: No equipment exists with the alias or id 'unknown-alias'

    >>> register[243]
    Traceback (most recent call last):
    ...
    IndexError: list index out of range

    ```

    whereas these errors can be silenced by using the [get][msl.equipment.schema.Register.get] method

    ```pycon
    >>> assert register.get("unknown") is None
    >>> assert register.get(243) is None

    ```

    Args:
        item: The index number, equipment id value or the equipment alias value in the register.

    Returns:
        The [Equipment][msl.equipment.schema.Equipment] item if `item` is valid, otherwise `None`.
    """
    try:
        return self[item]
    except (ValueError, IndexError):
        return None

tree ¤

tree(
    namespace: str | None = "DEFAULT", indent: int = 4
) -> ElementTree[Element[str]]

Convert the Register class into an XML element tree.

Parameters:

Name Type Description Default
namespace str | None

The namespace to associate with the root element. If the value is DEFAULT, uses the value of NAMESPACE as the namespace. If None, or an empty string, no namespace is associated with the root element.

'DEFAULT'
indent int

The number of spaces to indent sub elements. The value must be ≥ 0. This parameter is ignored if the version of Python is < 3.9.

4

Returns:

Type Description
ElementTree[Element[str]]

The Register as an ElementTree.

Source code in src/msl/equipment/schema.py
3001
3002
3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
def tree(self, namespace: str | None = "DEFAULT", indent: int = 4) -> ElementTree[Element[str]]:
    """Convert the [Register][msl.equipment.schema.Register] class into an XML element tree.

    Args:
        namespace: The namespace to associate with the root element. If the value is
            `DEFAULT`, uses the value of [NAMESPACE][msl.equipment.schema.Register.NAMESPACE]
            as the namespace. If `None`, or an empty string, no namespace is associated
            with the root element.
        indent: The number of spaces to indent sub elements. The value must be &ge; 0.
            This parameter is ignored if the version of Python is &lt; 3.9.

    Returns:
        The [Register][msl.equipment.schema.Register] as an
            [ElementTree][xml.etree.ElementTree.ElementTree].
    """
    if indent < 0:
        msg = f"Indentation must be >= 0, got {indent}"
        raise ValueError(msg)

    attrib = {"team": self.team}
    if namespace:
        if namespace == "DEFAULT":
            namespace = self.NAMESPACE
        attrib["xmlns"] = namespace

    # The <table><data> element is 7 levels deep from <register>
    _Indent.table_data = (7 * indent) + len("<data>")

    e = Element("register", attrib=attrib)
    e.extend(equipment.to_xml() for equipment in self)
    tree: ElementTree[Element[str]] = ElementTree(element=e)

    if indent > 0 and sys.version_info >= (3, 9):
        from xml.etree.ElementTree import indent as pretty  # noqa: PLC0415

        pretty(tree, space=" " * indent)

    return tree