#!/usr/bin/env python
#
# parser.py
"""
MassHunter worklist parser.
"""
#
# Copyright © 2020-2021 Dominic Davis-Foster <dominic@davis-foster.co.uk>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
# OR OTHER DEALINGS IN THE SOFTWARE.
#
# stdlib
import datetime
from typing import Any, Dict, Optional
# 3rd party
import lxml.objectify # type: ignore[import-untyped]
# this package
from mh_utils.utils import as_path, element_to_bool, strip_string
from mh_utils.worklist_parser.columns import Column, columns
__all__ = ["sample_info_tags", "parse_datetime", "parse_sample_info", "parse_params"]
#: Mapping of XML tag names to attribute names.
sample_info_tags: Dict[str, str] = {
# Tag Name: Attribute
"Identifier": "Sample ID",
"Name": "Sample Name",
"RackCode": "Rack Code",
"RackPosition": "Rack Position",
"PlateCode": "Plate Code",
"PlatePosition": "Plate Position",
"SamplePosition": "Sample Position",
"AcqMethod": "Method",
"DAMethod": "Override DA Method",
"DataFileName": "Data File",
"SampleType": "Sample Type",
"MethodExecutionType": "Method Type",
"BalanceType": "Balance Override",
"InjectionVolume": "Inj Vol (µl)",
"EquilibrationTime": "Equilib Time (min)",
"DilutionFactor": "Dilution",
"WeightPerVolume": "Wt/Vol",
"Description": "Comment",
"Barcode": "Barcode",
"Reserved1": "Reserved1",
"Reserved2": "Reserved2",
"Reserved3": "Reserved3",
"Reserved4": "Reserved4",
"Reserved5": "Reserved5",
"Reserved6": "Reserved6",
"CalibLevelName": "Level Name",
"SampleGroup": "Sample Group",
"SampleInformation": "Info.",
}
[docs]def parse_datetime(the_date: str) -> datetime.datetime:
"""
Parse a datetime from a worklist or contents file.
:param the_date: The date and time as a string in the format ``%Y-%m-%dT%H:%M:%S%z``.
"""
the_date = strip_string(the_date)
if the_date:
if ':' == the_date[-3]:
the_date = the_date[:-3] + the_date[-2:]
the_date = the_date[:19] + the_date[-5:]
return datetime.datetime.strptime(the_date, "%Y-%m-%dT%H:%M:%S%z")
else:
return datetime.datetime.fromtimestamp(0, tz=datetime.timezone.utc)
parse_worklist_datetime = parse_datetime
[docs]def parse_sample_info(
element: lxml.objectify.ObjectifiedElement,
user_columns: Optional[Dict[str, Column]] = None,
) -> Dict[str, Any]:
"""
Parse information about a sample in a worklist from XML.
:param element: The XML element to parse the data from
:param user_columns: Optional mapping of user column labels to
:class:`~mh_utils.worklist_parser.columns.Column` objects.
"""
sample_info: Dict[str, Any] = {}
if user_columns is None:
user_columns = {}
sample_info["Acquired Time"] = parse_datetime(element.AcqTime)
sample_info["Sample Locked Run Mode"] = element_to_bool(element.SampleLockedRunMode)
sample_info["Run Completed"] = element_to_bool(element.RunCompletedFlag)
sample_info["Label"] = str(element.Label).strip()
# SystemDefined attributes
for tag_name, attr_name in sample_info_tags.items():
column = columns[attr_name]
sample_info[attr_name] = column.cast_value(str(getattr(element, tag_name)).strip())
# SystemUsed and UserAdded attributes
for tag in element.iterchildren("SampleDataArray"):
for attr_name, column in user_columns.items():
if column.attribute_id == int(tag.AttributeID):
sample_info[attr_name] = column.cast_value(str(tag.DataValue).strip())
return sample_info
[docs]def parse_params(element: lxml.objectify.ObjectifiedElement) -> Dict[str, Any]:
"""
Parse the worklist execution parameters from XML.
:param element:
:return: Mapping of keys to parameter values.
"""
# this package
from mh_utils.worklist_parser.classes import Macro
params = dict(
operator_name=str(element.OperatorName),
# DataFileName=element.DataFileName,
run_type=int(element.RunType), # TODO: Enum, once values decoded
method_execution_type=str(element.MethodExecutionType),
acq_method_path=as_path(element.AcqMethodPath),
da_method_path=as_path(element.DAMethodPath),
export_output_path=as_path(element.ExportOutputPath),
combine_export_output=element_to_bool(element.CombineExportOutput),
combined_export_output_file=as_path(element.CombinedExportOutputFile),
combine_output_by_plate=element_to_bool(element.CombineOutputByPlate),
synchronous_execution=element_to_bool(element.SynchronousExecution),
stop_worklist_on_da_error=element_to_bool(element.StopWorklistOnDAError),
overlapped_injections=element_to_bool(element.OverlappedInjections),
use_barcode=element_to_bool(element.UseBarcode),
inject_on_barcode_mismatch=element_to_bool(element.InjectOnBarcodeMismatch),
threshold_disk_space=int(element.ThresholdDiskSpace),
ready_time_out=int(element.ReadyTimeOut),
clear_run_checkbox=element_to_bool(element.ClearRunCheckBox),
use_pre_worklist_macro=element_to_bool(element.UsePreWorklistMacro),
pre_worklist_macro=Macro.from_xml(element.PreWorklistMacro),
use_post_worklist_macro=element_to_bool(element.UsePostWorklistMacro),
post_worklist_macro=Macro.from_xml(element.PostWorklistMacro),
run_acq_clean_macro_on_error=element_to_bool(element.RunAcqCleanMacroOnError),
acq_clean_macro=Macro.from_xml(element.AcqCleanMacro),
use_post_analysis_macro=element_to_bool(element.UsePostAnalysisMacro),
post_analysis_macro=Macro.from_xml(element.PostAnalysisMacro),
description=str(element.Description).strip(),
plate_bar_codes=element.PlateBarCodes, # TODO: determine type
)
return params