Skip to content

Register¤

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 are equipment registers.

()
Source code in src/msl/equipment/schema.py
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
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 are equipment registers.
    """
    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
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
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 attributes 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
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
def find(self, pattern: str | re.Pattern[str], *, flags: int = 0) -> Iterator[Equipment]:  # noqa: C901
    """Find equipment in the register.

    The following attributes 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.deserialised)

    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
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
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/resources/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
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
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