Additional utilities

Run all checks on the implementations package.

Source code in utils\check_implementations.py
23
24
25
26
27
28
29
30
def check_implementations() -> None:
    """Run all checks on the `implementations` package."""
    _, impl_root = _require_implementations_package()
    _check_directory_structure(impl_root)
    modules = _import_impl_modules()
    _check_contract_implementations(modules)
    _check_device_ui_files(impl_root, modules["devices_workers"])
    _check_config_file(impl_root)

Helper for mapping message level names to QColor instances.

The colours are used by the GUI console to display messages with different visual emphasis (alert, warning, normal).

Source code in utils\console_colours.py
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class ConsoleColours:
    """
    Helper for mapping message level names to QColor instances.

    The colours are used by the GUI console to display messages with different
    visual emphasis (alert, warning, normal).
    """
    def __init__(self):
        self._alert = QtGui.QColor(255, 0, 0)
        self._warning = QtGui.QColor(255, 127, 0)
        self._normal = QtGui.QColor(255,255,255)

    def get_colour(self, level):
        return getattr(self, f"_{level}")

Utility for parsing and serialising my custom timestamp format.

The helper supports flexible separators and format strings used to construct filenames and labels, and provides round-tripping between strings and :class:datetime.datetime objects.

Source code in utils\custom_datetime.py
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
class CustomDatetime:
    """
    Utility for parsing and serialising my custom timestamp format.

    The helper supports flexible separators and format strings used to construct
    filenames and labels, and provides round-tripping between strings and
    :class:`datetime.datetime` objects.
    """
    def __init__(self,
                 separators="-_",
                 label_format="%Y_%m_%d_%H_%M_%S",
                 default_time="09_00_00",
                 date_pattern=None,
                 time_pattern=None):

        self.separators = separators
        self.label_format = label_format
        self.default_time = default_time

        # If user didn't pass custom patterns, use default YYYY_MM_DD / HH_MM_SS
        self.date_pattern = date_pattern or rf"(\d{{4}})[{self.separators}](\d{{2}})[{self.separators}](\d{{2}})"
        self.time_pattern = time_pattern or rf"(\d{{2}})[{self.separators}](\d{{2}})[{self.separators}](\d{{2}})"

    def create_datetime_from_string(self, input_datetime_str: str = None) -> datetime:
        """
        Creates a datetime object from an input datetime string.

        - Infers %Y vs %y from the year token length.
        - Allows HH_MM or HH_MM_SS; pads seconds to 00 when missing.
        """
        if input_datetime_str is None:
            raise ValueError("Input datetime string cannot be None")

        # --- Find date ---
        date_match = re.search(self.date_pattern, input_datetime_str)
        if not date_match:
            raise ValueError(f"Input string {input_datetime_str} does not contain a valid date")

        date_groups = date_match.groups()
        if len(date_groups) != 3:
            raise ValueError("Date pattern must capture exactly 3 groups (Y, m, d)")

        year_token = date_groups[0]
        if len(year_token) == 4:
            date_fmt = "%Y_%m_%d"
        elif len(year_token) == 2:
            date_fmt = "%y_%m_%d"
        else:
            raise ValueError("Year group must be 2 or 4 digits")

        date_str = "_".join(date_groups)

        # --- Find time (search only after date to avoid picking up pre-date tokens) ---
        remaining_str = input_datetime_str[date_match.end():]
        time_match = re.search(self.time_pattern, remaining_str)

        if time_match:
            time_groups = time_match.groups()
            if len(time_groups) == 3:
                time_str = "_".join(time_groups)  # HH_MM_SS
                time_fmt = "%H_%M_%S"
            elif len(time_groups) == 2:
                time_str = f"{time_groups[0]}_{time_groups[1]}_00"  # HH_MM_00
                time_fmt = "%H_%M_%S"
            else:
                raise ValueError("Time pattern must capture 2 (H,M) or 3 (H,M,S) groups")
        else:
            # Use default time (assumed HH_MM_SS like "09_00_00")
            time_str = self.default_time
            time_fmt = "%H_%M_%S"

        # Final assemble + parse
        datetime_str = f"{date_str}_{time_str}"
        fmt = f"{date_fmt}_{time_fmt}"
        return datetime.strptime(datetime_str, fmt)

    def write_datetime_to_string(self, input_datetime: datetime) -> str:
        if input_datetime is None:
            raise ValueError("Input datetime cannot be None")
        return input_datetime.strftime(self.label_format)

    def get_current_timestamp(self, now: datetime | None = None) -> str:
        """
        Returns a timestamp string using `label_format`, suitable for filenames.
        Pass `now` for deterministic testing; otherwise uses current local time.
        """
        current = now or datetime.now()
        return self.write_datetime_to_string(current)

create_datetime_from_string(input_datetime_str=None)

Creates a datetime object from an input datetime string.

  • Infers %Y vs %y from the year token length.
  • Allows HH_MM or HH_MM_SS; pads seconds to 00 when missing.
Source code in utils\custom_datetime.py
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
def create_datetime_from_string(self, input_datetime_str: str = None) -> datetime:
    """
    Creates a datetime object from an input datetime string.

    - Infers %Y vs %y from the year token length.
    - Allows HH_MM or HH_MM_SS; pads seconds to 00 when missing.
    """
    if input_datetime_str is None:
        raise ValueError("Input datetime string cannot be None")

    # --- Find date ---
    date_match = re.search(self.date_pattern, input_datetime_str)
    if not date_match:
        raise ValueError(f"Input string {input_datetime_str} does not contain a valid date")

    date_groups = date_match.groups()
    if len(date_groups) != 3:
        raise ValueError("Date pattern must capture exactly 3 groups (Y, m, d)")

    year_token = date_groups[0]
    if len(year_token) == 4:
        date_fmt = "%Y_%m_%d"
    elif len(year_token) == 2:
        date_fmt = "%y_%m_%d"
    else:
        raise ValueError("Year group must be 2 or 4 digits")

    date_str = "_".join(date_groups)

    # --- Find time (search only after date to avoid picking up pre-date tokens) ---
    remaining_str = input_datetime_str[date_match.end():]
    time_match = re.search(self.time_pattern, remaining_str)

    if time_match:
        time_groups = time_match.groups()
        if len(time_groups) == 3:
            time_str = "_".join(time_groups)  # HH_MM_SS
            time_fmt = "%H_%M_%S"
        elif len(time_groups) == 2:
            time_str = f"{time_groups[0]}_{time_groups[1]}_00"  # HH_MM_00
            time_fmt = "%H_%M_%S"
        else:
            raise ValueError("Time pattern must capture 2 (H,M) or 3 (H,M,S) groups")
    else:
        # Use default time (assumed HH_MM_SS like "09_00_00")
        time_str = self.default_time
        time_fmt = "%H_%M_%S"

    # Final assemble + parse
    datetime_str = f"{date_str}_{time_str}"
    fmt = f"{date_fmt}_{time_fmt}"
    return datetime.strptime(datetime_str, fmt)

get_current_timestamp(now=None)

Returns a timestamp string using label_format, suitable for filenames. Pass now for deterministic testing; otherwise uses current local time.

Source code in utils\custom_datetime.py
87
88
89
90
91
92
93
def get_current_timestamp(self, now: datetime | None = None) -> str:
    """
    Returns a timestamp string using `label_format`, suitable for filenames.
    Pass `now` for deterministic testing; otherwise uses current local time.
    """
    current = now or datetime.now()
    return self.write_datetime_to_string(current)

Export matrix to a delimiter-separated text file.

Parameters

filename: Path to the output file that will be created/overwritten. list_of_lists: List of equal-length iterables, each representing a column of data. header: List of column labels written as the first line of the file. delimiter: String used to join header fields and row values (defaults to tab).

Notes

The function assumes that all columns in list_of_lists have the same length and will raise IndexError if this is not the case.

Source code in utils\export_to_csv.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def export_to_csv(filename: str, list_of_lists: list, header: list, delimiter: str = '\t'):
    """
    Export matrix to a delimiter-separated text file.

    Parameters
    ----------
    filename:
        Path to the output file that will be created/overwritten.
    list_of_lists:
        List of equal-length iterables, each representing a column of data.
    header:
        List of column labels written as the first line of the file.
    delimiter:
        String used to join header fields and row values (defaults to tab).

    Notes
    -----
    The function assumes that all columns in ``list_of_lists`` have the same
    length and will raise ``IndexError`` if this is not the case.
    """
    first_list = list_of_lists[0]

    # Open a file
    with open(filename, 'w') as csv_file:
        # Save the header row
        csv_file.write(delimiter.join(header))
        csv_file.write('\n')

        # Go through all columns and write the data
        for index in range(len(first_list)):
            row = []
            for sublist in list_of_lists:
                row.append(str(sublist[index]))
            csv_file.write(delimiter.join(row))
            csv_file.write('\n')

Read a JSON configuration file and return its contents as a dictionary.

Parameters

config_path : str The file path to the JSON configuration file.

Returns

dict A dictionary containing the configuration settings.

Source code in utils\read_config.py
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
def read_config(config_path: str) -> dict:
    """Read a JSON configuration file and return its contents as a dictionary.

    Parameters
    ----------
    config_path : str
        The file path to the JSON configuration file.

    Returns
    -------
    dict
        A dictionary containing the configuration settings.
    """
    with open(config_path, 'r') as file:
        config = json.load(file)
    return config