Additional utilities
Run all checks on the implementations package.
Source code in utils/check_implementations.py
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
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)
self._result = QtGui.QColor(0, 255, 127)
def get_colour(self, level):
level_name = f"_{level}"
try:
colour = getattr(self, level_name)
except AttributeError:
warnings.warn(f"ConsoleColours: Unknown level '{level}', defaulting to 'normal'")
level_name = "_normal"
return getattr(self, level_name)
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
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
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
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
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
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