ECGProcess 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ecgprocess/__init__.py +1 -0
- ecgprocess/_version.py +1 -0
- ecgprocess/constants.py +170 -0
- ecgprocess/errors.py +380 -0
- ecgprocess/example_data/__init__.py +1 -0
- ecgprocess/example_data/example_datasets/.example-XML1.xml.un~ +0 -0
- ecgprocess/example_data/example_datasets/.example-XML1.xsd.un~ +0 -0
- ecgprocess/example_data/example_datasets/.example-XML1_a.xsd.un~ +0 -0
- ecgprocess/example_data/example_datasets/.example-XML1_d.xsd.un~ +0 -0
- ecgprocess/example_data/example_datasets/.example-XML1_wrong.xsd.un~ +0 -0
- ecgprocess/example_data/example_datasets/example-DICOM1.dcm +0 -0
- ecgprocess/example_data/example_datasets/example-DICOM2.dcm +0 -0
- ecgprocess/example_data/example_datasets/example-XML1.xml +675 -0
- ecgprocess/example_data/example_datasets/example-XML1.xsd +518 -0
- ecgprocess/example_data/example_datasets/example-XML1_error.xsd +519 -0
- ecgprocess/example_data/example_datasets/example-XML1_wrong.xsd +519 -0
- ecgprocess/example_data/example_datasets/string_data.txt +1 -0
- ecgprocess/example_data/examples.py +501 -0
- ecgprocess/plot_ecgs.py +921 -0
- ecgprocess/process_dicom.py +243 -0
- ecgprocess/process_xml.py +319 -0
- ecgprocess/tabular.py +1394 -0
- ecgprocess/utils/__init__.py +1 -0
- ecgprocess/utils/config_tools.py +581 -0
- ecgprocess/utils/ecg_tools.py +311 -0
- ecgprocess/utils/engineering_tools.py +385 -0
- ecgprocess/utils/general.py +399 -0
- ecgprocess/utils/reader_tools.py +508 -0
- ecgprocess-1.1.0.dist-info/METADATA +153 -0
- ecgprocess-1.1.0.dist-info/RECORD +33 -0
- ecgprocess-1.1.0.dist-info/WHEEL +5 -0
- ecgprocess-1.1.0.dist-info/licenses/LICENCE.txt +17 -0
- ecgprocess-1.1.0.dist-info/top_level.txt +1 -0
ecgprocess/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from ._version import __version__
|
ecgprocess/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '1.1.0'
|
ecgprocess/constants.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
'''
|
|
2
|
+
Constants used by ECGProcess
|
|
3
|
+
'''
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
# %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
8
|
+
class CoreData:
|
|
9
|
+
'''
|
|
10
|
+
Core variable names which are used internally by various modules in
|
|
11
|
+
`ECGProcess`.
|
|
12
|
+
'''
|
|
13
|
+
# /////////////////////////////////////////////////////////////////////////
|
|
14
|
+
class DataTypes:
|
|
15
|
+
'''
|
|
16
|
+
Contains constants representing high-level data categories used
|
|
17
|
+
in ECG processing, such as waveforms, median beats, and metadata.
|
|
18
|
+
'''
|
|
19
|
+
WaveForms = 'WaveForms'
|
|
20
|
+
MedianBeats = 'MedianBeats'
|
|
21
|
+
MetaData = 'MetaData'
|
|
22
|
+
OtherData = 'OtherData'
|
|
23
|
+
# /////////////////////////////////////////////////////////////////////////
|
|
24
|
+
class Leads:
|
|
25
|
+
'''
|
|
26
|
+
The internally used lead names.
|
|
27
|
+
'''
|
|
28
|
+
I = 'I'
|
|
29
|
+
II = 'II'
|
|
30
|
+
III = 'III'
|
|
31
|
+
aVR = 'aVR'
|
|
32
|
+
aVL = 'aVL'
|
|
33
|
+
aVF = 'aVF'
|
|
34
|
+
V1 = 'V1'
|
|
35
|
+
V2 = 'V2'
|
|
36
|
+
V3 = 'V3'
|
|
37
|
+
V4 = 'V4'
|
|
38
|
+
V5 = 'V5'
|
|
39
|
+
V6 = 'V6'
|
|
40
|
+
# /////////////////////////////////////////////////////////////////////////
|
|
41
|
+
class MetaData:
|
|
42
|
+
'''
|
|
43
|
+
Contains constants representing metadata fields required for
|
|
44
|
+
ECG processing.
|
|
45
|
+
'''
|
|
46
|
+
UID = 'unique identifier'
|
|
47
|
+
NLEADS = 'number of leads'
|
|
48
|
+
RES_U_W = 'resolution unit (waveforms)'
|
|
49
|
+
RES_U_M = 'resolution unit (medianbeats)'
|
|
50
|
+
RES_W = 'resolution (waveforms)'
|
|
51
|
+
RES_M = 'resolution (medianbeats)'
|
|
52
|
+
SF = 'sampling frequency (original)'
|
|
53
|
+
SF_U = 'sampling frequency unit'
|
|
54
|
+
SN_W = 'sampling number (waveforms)'
|
|
55
|
+
SN_M = 'sampling number (medianbeats)'
|
|
56
|
+
# /////////////////////////////////////////////////////////////////////////
|
|
57
|
+
class ProcessingData:
|
|
58
|
+
'''
|
|
59
|
+
Contains constants for attributes and variables created by the process
|
|
60
|
+
modules.
|
|
61
|
+
'''
|
|
62
|
+
TAGS = 'tags'
|
|
63
|
+
RAW = 'raw_data'
|
|
64
|
+
Duration = 'duration (sec)'
|
|
65
|
+
SF_NEW = 'sampling frequency (processed)'
|
|
66
|
+
RESAMPLE = 'resample_500'
|
|
67
|
+
AUG_LEADS = 'augment_leads'
|
|
68
|
+
|
|
69
|
+
# %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
70
|
+
class UtilsReaderNames(object):
|
|
71
|
+
'''
|
|
72
|
+
Names used in the Utils Reader Tools module.
|
|
73
|
+
'''
|
|
74
|
+
DICOM_WAVE_ARRAY = 'waveform_array'
|
|
75
|
+
|
|
76
|
+
# %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
77
|
+
class UtilsGeneralNames(object):
|
|
78
|
+
'''
|
|
79
|
+
Names used in the Utils General module.
|
|
80
|
+
'''
|
|
81
|
+
LXML_ERROR = 'expected'
|
|
82
|
+
|
|
83
|
+
# %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
84
|
+
class UtilsConfigData(object):
|
|
85
|
+
'''
|
|
86
|
+
Names used in the Utils Config Data module
|
|
87
|
+
'''
|
|
88
|
+
MetaData = CoreData.DataTypes.MetaData
|
|
89
|
+
OtherData = CoreData.DataTypes.OtherData
|
|
90
|
+
WaveForms = CoreData.DataTypes.WaveForms
|
|
91
|
+
MedianBeats = CoreData.DataTypes.MedianBeats
|
|
92
|
+
path = 'path'
|
|
93
|
+
data = '_data'
|
|
94
|
+
MAPPER = 'mapper'
|
|
95
|
+
|
|
96
|
+
# %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
97
|
+
class ProcessXMLNames(object):
|
|
98
|
+
'''
|
|
99
|
+
Names used in the process_xml module.
|
|
100
|
+
'''
|
|
101
|
+
RESAMPLE = CoreData.ProcessingData.RESAMPLE
|
|
102
|
+
AUG_LEADS = CoreData.ProcessingData.AUG_LEADS
|
|
103
|
+
|
|
104
|
+
# %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
105
|
+
class ProcessDicomNames(object):
|
|
106
|
+
RESAMPLE = CoreData.ProcessingData.RESAMPLE
|
|
107
|
+
AUG_LEADS = CoreData.ProcessingData.AUG_LEADS
|
|
108
|
+
|
|
109
|
+
# %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
110
|
+
class TabularNames(object):
|
|
111
|
+
E_META = 'extract_'+CoreData.DataTypes.MetaData
|
|
112
|
+
E_WAVE = 'extract_'+CoreData.DataTypes.WaveForms
|
|
113
|
+
E_MEDIAN = 'extract_'+CoreData.DataTypes.MedianBeats
|
|
114
|
+
ENG_META = 'engineer_'+CoreData.DataTypes.MetaData
|
|
115
|
+
ENG_WAVE = 'engineer_'+CoreData.DataTypes.WaveForms
|
|
116
|
+
ENG_MEDIAN = 'engineer_'+CoreData.DataTypes.MedianBeats
|
|
117
|
+
META_DICT = 'meta_dict'
|
|
118
|
+
SKIP_PERM = 'ignore_permission'
|
|
119
|
+
SKIP_DATA = 'ignore_data'
|
|
120
|
+
SKIP_INVALID = 'ignore_invalid'
|
|
121
|
+
CONFIRM_META = 'confirm_meta'
|
|
122
|
+
CONFIRM_WAVE = 'confirm_wave'
|
|
123
|
+
CONFIRM_MEDIAN = 'confirm_median'
|
|
124
|
+
SCHEMA = 'schema'
|
|
125
|
+
RPATH_L = 'raw_path_list'
|
|
126
|
+
FPATH_L = 'failed_path_list'
|
|
127
|
+
CPATH_L = 'curated_path_list'
|
|
128
|
+
KEY_L = 'key_list'
|
|
129
|
+
NO_DATA_L = 'no_data_list'
|
|
130
|
+
INVALID_L = 'invalid_list'
|
|
131
|
+
TABLE_CALLED = 'table_called'
|
|
132
|
+
WRITE_TAB = 'table'
|
|
133
|
+
WRITE_NUMPY = 'numpy'
|
|
134
|
+
WRITE_TFLOW = 'tensorflow'
|
|
135
|
+
WRITE_ECG_PATH = 'target_path'
|
|
136
|
+
WRITE_TYPE = 'file_type'
|
|
137
|
+
ID_START = '_id_start_number'
|
|
138
|
+
KEY = 'key'
|
|
139
|
+
FILENAM = 'filename'
|
|
140
|
+
SIG_LEN_W = 'signal_length_w'
|
|
141
|
+
SIG_LEN_M = 'signal_length_m'
|
|
142
|
+
PAD_VALUE = 'pad_value'
|
|
143
|
+
SN_W_PADDED = 'sampling number padded (waveforms)'
|
|
144
|
+
SN_M_PADDED = 'sampling number padded (medianbeats)'
|
|
145
|
+
DURATION_PADDED = 'duration padded (sec)'
|
|
146
|
+
|
|
147
|
+
# %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
148
|
+
class PlotNames:
|
|
149
|
+
'''
|
|
150
|
+
Constants used by the ECGDrawing plotting module.
|
|
151
|
+
'''
|
|
152
|
+
MILLIVOLT = 'millivolt'
|
|
153
|
+
MICROVOLT = 'microvolt'
|
|
154
|
+
LEAD_VOLTAGES = 'lead_voltages'
|
|
155
|
+
START_POS_FIRST = 'first'
|
|
156
|
+
# #### standard ECG paper grid dimensions (mm)
|
|
157
|
+
MINOR_GRID_MM = 1.0
|
|
158
|
+
MAJOR_GRID_MM = 5.0
|
|
159
|
+
# #### ECGDrawing slot names
|
|
160
|
+
DEFAULT_LEADS = '_default_leads'
|
|
161
|
+
FIGURE = 'figure'
|
|
162
|
+
AXES = 'axes'
|
|
163
|
+
LAYOUT_MAPPER = 'layout_mapper'
|
|
164
|
+
STYLE = 'style'
|
|
165
|
+
MINOR_STEP_X = 'minor_step_x'
|
|
166
|
+
MAJOR_STEP_X = 'major_step_x'
|
|
167
|
+
MINOR_STEP_Y = 'minor_step_y'
|
|
168
|
+
MAJOR_STEP_Y = 'major_step_y'
|
|
169
|
+
PAPER_SPEED = 'paper_speed'
|
|
170
|
+
MM_PER_MV = 'mm_per_mv'
|
ecgprocess/errors.py
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
'''
|
|
2
|
+
Error handling for ECGProcess.
|
|
3
|
+
'''
|
|
4
|
+
import os
|
|
5
|
+
import inspect
|
|
6
|
+
import warnings
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from os import PathLike
|
|
10
|
+
from typing import Any, Type, Union
|
|
11
|
+
|
|
12
|
+
# %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
13
|
+
|
|
14
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
15
|
+
class InputValidationError(Exception): # pragma: no cover
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
19
|
+
def get_param_name(param:Any) -> str | None: # pragma: no cover
|
|
20
|
+
'''
|
|
21
|
+
Gets the name of `param` from the caller's local scope.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
param : `Any`
|
|
26
|
+
The object whose variable name is to be resolved.
|
|
27
|
+
|
|
28
|
+
Returns
|
|
29
|
+
-------
|
|
30
|
+
name : `str` or `None`
|
|
31
|
+
The variable name as a string, or `None` if it cannot be resolved.
|
|
32
|
+
'''
|
|
33
|
+
frame = inspect.currentframe().f_back.f_back
|
|
34
|
+
param_names =\
|
|
35
|
+
[name for name, value in frame.f_locals.items() if value is param]
|
|
36
|
+
return param_names[0] if param_names else None
|
|
37
|
+
|
|
38
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
39
|
+
def is_type(param: Any, types: tuple[Type] | Type,
|
|
40
|
+
param_name: str | None=None) -> bool:
|
|
41
|
+
"""
|
|
42
|
+
Checks if a given parameter matches any of the supplied types.
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
param : `Any`
|
|
47
|
+
Object to test.
|
|
48
|
+
types : `Type` or `tuple` [`Type`]
|
|
49
|
+
Either a single type, or a tuple of types to test against.
|
|
50
|
+
param_name : `str` or `None`, optional
|
|
51
|
+
Name of the parameter, used in the error message. Resolved
|
|
52
|
+
automatically from the caller's scope when not supplied.
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
result : `bool`
|
|
57
|
+
`True` if the parameter is an instance of any of the given types.
|
|
58
|
+
|
|
59
|
+
Raises
|
|
60
|
+
------
|
|
61
|
+
InputValidationError
|
|
62
|
+
If `param` is not an instance of any type in `types`.
|
|
63
|
+
"""
|
|
64
|
+
if not isinstance(param, types):
|
|
65
|
+
if param_name is None:
|
|
66
|
+
param_name = get_param_name(param)
|
|
67
|
+
raise InputValidationError(
|
|
68
|
+
f"Expected any of [{types}], "
|
|
69
|
+
f"got {type(param)}; Please see parameter: `{param_name}`."
|
|
70
|
+
)
|
|
71
|
+
return True
|
|
72
|
+
|
|
73
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
74
|
+
def is_df(df: Any) -> bool: # pragma: no cover
|
|
75
|
+
"""
|
|
76
|
+
Checks if object is a pd.DataFrame.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
df: `Any`
|
|
81
|
+
|
|
82
|
+
Returns
|
|
83
|
+
-------
|
|
84
|
+
True if the df is a pd.DataFrame. Raises InputValidationError otherwise.
|
|
85
|
+
"""
|
|
86
|
+
return is_type(df, pd.DataFrame)
|
|
87
|
+
|
|
88
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
89
|
+
def is_series_type(column: pd.Series | pd.DataFrame,
|
|
90
|
+
types: tuple[Type] | Type,
|
|
91
|
+
) -> bool:
|
|
92
|
+
"""
|
|
93
|
+
Checks if a `pd.DataFrame` or `pd.Series` contains the supplied type.
|
|
94
|
+
|
|
95
|
+
Parameters
|
|
96
|
+
----------
|
|
97
|
+
column : `pd.Series` or `pd.DataFrame`
|
|
98
|
+
The column or frame whose elements are to be tested.
|
|
99
|
+
types : `Type` or `tuple` [`Type`]
|
|
100
|
+
Either a single type, or a tuple of types to test against.
|
|
101
|
+
|
|
102
|
+
Returns
|
|
103
|
+
-------
|
|
104
|
+
result : `bool`
|
|
105
|
+
`True` if all elements match any of the given types.
|
|
106
|
+
|
|
107
|
+
Raises
|
|
108
|
+
------
|
|
109
|
+
InputValidationError
|
|
110
|
+
If any element is not an instance of any type in `types`.
|
|
111
|
+
|
|
112
|
+
Notes
|
|
113
|
+
-----
|
|
114
|
+
Tests each element individually rather than relying on dtype inference.
|
|
115
|
+
"""
|
|
116
|
+
# check input
|
|
117
|
+
is_type(column, (pd.DataFrame, pd.Series))
|
|
118
|
+
# run tests
|
|
119
|
+
if isinstance(column, pd.Series):
|
|
120
|
+
[is_type(col, types) for col in column]
|
|
121
|
+
elif isinstance(column, pd.DataFrame):
|
|
122
|
+
for _, col in column.items():
|
|
123
|
+
[is_type(co, types) for co in col]
|
|
124
|
+
# return
|
|
125
|
+
return True
|
|
126
|
+
|
|
127
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
128
|
+
def same_len(object1: Any, object2: Any,
|
|
129
|
+
object_names:list[str] | None=None,
|
|
130
|
+
) -> bool:
|
|
131
|
+
"""
|
|
132
|
+
Check if two objects have the same length.
|
|
133
|
+
|
|
134
|
+
Parameters
|
|
135
|
+
----------
|
|
136
|
+
object1, object2 : `Any`
|
|
137
|
+
Any type of object with a `__len__` method.
|
|
138
|
+
object_names : `list` [`str`] or `None`, optional
|
|
139
|
+
Names of the two objects, included in the error message when
|
|
140
|
+
lengths differ.
|
|
141
|
+
|
|
142
|
+
Returns
|
|
143
|
+
-------
|
|
144
|
+
result : `bool`
|
|
145
|
+
`True` if both objects have the same length.
|
|
146
|
+
|
|
147
|
+
Raises
|
|
148
|
+
------
|
|
149
|
+
ValueError
|
|
150
|
+
If the lengths differ, or if `object_names` does not contain
|
|
151
|
+
exactly two strings.
|
|
152
|
+
"""
|
|
153
|
+
n1 = len(object1)
|
|
154
|
+
n2 = len(object2)
|
|
155
|
+
if object_names is None:
|
|
156
|
+
object_names = ['object1', 'object2']
|
|
157
|
+
elif len(object_names) !=2:
|
|
158
|
+
raise ValueError('`object_names` should be `NoneType` or contain '
|
|
159
|
+
'two strings')
|
|
160
|
+
# the actual test
|
|
161
|
+
if n1 != n2:
|
|
162
|
+
raise ValueError(
|
|
163
|
+
"The length of `{0}`: {1}, does not match "
|
|
164
|
+
"the length of `{2}`: {3}.".format(
|
|
165
|
+
object_names[0], n1, object_names[1], n2)
|
|
166
|
+
)
|
|
167
|
+
return True
|
|
168
|
+
|
|
169
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
170
|
+
def are_columns_in_df(
|
|
171
|
+
df: pd.DataFrame, expected_columns: list[str] | str,
|
|
172
|
+
warning: bool=False
|
|
173
|
+
) -> bool:
|
|
174
|
+
"""
|
|
175
|
+
Checks if all expected columns are present in a `pandas.DataFrame`.
|
|
176
|
+
|
|
177
|
+
Parameters
|
|
178
|
+
----------
|
|
179
|
+
df : `pandas.DataFrame`
|
|
180
|
+
The DataFrame to inspect.
|
|
181
|
+
expected_columns : `list` [`str`] or `str`
|
|
182
|
+
A single column name or a list of column names to check for.
|
|
183
|
+
warning : `bool`, default `False`
|
|
184
|
+
If `True`, issue a `UserWarning` instead of raising an error.
|
|
185
|
+
|
|
186
|
+
Returns
|
|
187
|
+
-------
|
|
188
|
+
result : `bool`
|
|
189
|
+
`True` if all expected columns are present; `False` if any are
|
|
190
|
+
missing and `warning` is `True`.
|
|
191
|
+
|
|
192
|
+
Raises
|
|
193
|
+
------
|
|
194
|
+
InputValidationError
|
|
195
|
+
If any expected columns are absent and `warning` is `False`.
|
|
196
|
+
"""
|
|
197
|
+
# constant
|
|
198
|
+
message = "The following columns are missing from the pandas.DataFrame: {}"
|
|
199
|
+
res = True
|
|
200
|
+
# tests
|
|
201
|
+
expected_columns_set: set[str] = set(expected_columns) if isinstance(
|
|
202
|
+
expected_columns, list
|
|
203
|
+
) else set([expected_columns])
|
|
204
|
+
|
|
205
|
+
missing_columns = expected_columns_set - set(df.columns)
|
|
206
|
+
# return
|
|
207
|
+
if missing_columns:
|
|
208
|
+
if warning == False:
|
|
209
|
+
raise InputValidationError(
|
|
210
|
+
message.format(missing_columns)
|
|
211
|
+
)
|
|
212
|
+
else:
|
|
213
|
+
warnings.warn(
|
|
214
|
+
message.format(missing_columns)
|
|
215
|
+
)
|
|
216
|
+
res = False
|
|
217
|
+
return res
|
|
218
|
+
|
|
219
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
220
|
+
def string_to_list(object:Any) -> Any | list[str]:
|
|
221
|
+
'''
|
|
222
|
+
Checks if `object` is a string and wraps this in a list, returns the
|
|
223
|
+
original object if it is not a string.
|
|
224
|
+
|
|
225
|
+
Parameters
|
|
226
|
+
----------
|
|
227
|
+
object : `Any`
|
|
228
|
+
Any object that might be a string.
|
|
229
|
+
|
|
230
|
+
Returns
|
|
231
|
+
-------
|
|
232
|
+
string wrapped in a list or the original object type.
|
|
233
|
+
'''
|
|
234
|
+
if isinstance(object, str):
|
|
235
|
+
return [object]
|
|
236
|
+
else:
|
|
237
|
+
return object
|
|
238
|
+
|
|
239
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
240
|
+
def _check_readable(path: str | PathLike) -> None: # pragma: no cover
|
|
241
|
+
"""
|
|
242
|
+
Do we have read access. Note this should work on a directory or file.
|
|
243
|
+
|
|
244
|
+
Parameters
|
|
245
|
+
----------
|
|
246
|
+
path: `str` or `PathLike`
|
|
247
|
+
A path to a directory or file.
|
|
248
|
+
"""
|
|
249
|
+
if os.access(path, os.R_OK) == False:
|
|
250
|
+
raise PermissionError('You do not have read-access to: `{}`'.\
|
|
251
|
+
format(path))
|
|
252
|
+
|
|
253
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
254
|
+
def _check_presence(path: str | PathLike) -> None:
|
|
255
|
+
"""
|
|
256
|
+
Check if a directory path exists.
|
|
257
|
+
|
|
258
|
+
Parameters
|
|
259
|
+
----------
|
|
260
|
+
path : `str` or `PathLike`
|
|
261
|
+
Path to a directory.
|
|
262
|
+
|
|
263
|
+
Raises
|
|
264
|
+
------
|
|
265
|
+
FileNotFoundError
|
|
266
|
+
If the path does not exist.
|
|
267
|
+
NotADirectoryError
|
|
268
|
+
If the path exists but is not a directory.
|
|
269
|
+
"""
|
|
270
|
+
if not os.path.exists(path):
|
|
271
|
+
raise FileNotFoundError(
|
|
272
|
+
f'The path `{path}` does not exist.'
|
|
273
|
+
)
|
|
274
|
+
if not os.path.isdir(path):
|
|
275
|
+
raise NotADirectoryError(
|
|
276
|
+
f'The path `{path}` exists but is not a directory.'
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
280
|
+
def _check_is_file(path: str | PathLike) -> None:
|
|
281
|
+
"""
|
|
282
|
+
Check if a file path exists.
|
|
283
|
+
|
|
284
|
+
Parameters
|
|
285
|
+
----------
|
|
286
|
+
path : `str` or `PathLike`
|
|
287
|
+
Path to a file.
|
|
288
|
+
|
|
289
|
+
Raises
|
|
290
|
+
------
|
|
291
|
+
FileNotFoundError
|
|
292
|
+
If the path does not exist.
|
|
293
|
+
IsADirectoryError
|
|
294
|
+
If the path exists but is not a regular file.
|
|
295
|
+
"""
|
|
296
|
+
if not os.path.exists(path):
|
|
297
|
+
raise FileNotFoundError(
|
|
298
|
+
f'The file `{path}` does not exist.'
|
|
299
|
+
)
|
|
300
|
+
if not os.path.isfile(path):
|
|
301
|
+
raise IsADirectoryError(
|
|
302
|
+
f'The path `{path}` exists but is not a regular file.'
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
306
|
+
# error messages
|
|
307
|
+
class Error_MSG(object): # pragma: no cover
|
|
308
|
+
'''
|
|
309
|
+
A collection of error messages.
|
|
310
|
+
'''
|
|
311
|
+
MISSING_SIGNAL = 'There is no `{}` data available.'
|
|
312
|
+
MISSING_DICOM='`{}` not found in DICOM file.'
|
|
313
|
+
MISSING_XML='`{}` not found in XML file.'
|
|
314
|
+
MISSING_KEY='key `{}` not found in object `{}`.'
|
|
315
|
+
MISSING_ATTR='attribute `{}` not found in object `{}`.'
|
|
316
|
+
CHOICE_PARM='`{}` is constrained to `{}`.'
|
|
317
|
+
MISSING_PATH_INSTANCE="Please supply either `path` or `{}`, but not both."
|
|
318
|
+
|
|
319
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
320
|
+
class Warn_MSG(object): # pragma: no cover
|
|
321
|
+
'''
|
|
322
|
+
A collection of warning messages.
|
|
323
|
+
'''
|
|
324
|
+
MISSING_TAG='The following tags could not be found: {}.'
|
|
325
|
+
NUMB_IS_NONE = '`{}` is NoneType, cannot convert to numerical.'
|
|
326
|
+
DROP_NONE_NUMERIC = ('The following non-numerical columns were '
|
|
327
|
+
'dropped when converting data to numpy '
|
|
328
|
+
'arrays: %s.')
|
|
329
|
+
RESAMPLE_NO_DURATION = (
|
|
330
|
+
'Resampling to 500 Hz was requested but `duration` could not '
|
|
331
|
+
'be calculated (sampling number or sampling frequency is '
|
|
332
|
+
'None); resampling skipped for this file.'
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
336
|
+
# error messages
|
|
337
|
+
class STDOUT_MSG(object): # pragma: no cover
|
|
338
|
+
'''
|
|
339
|
+
A collection of stdout messages.
|
|
340
|
+
'''
|
|
341
|
+
PROCESSING_PATH = 'Processing file: {}'
|
|
342
|
+
|
|
343
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
344
|
+
class MissingTagError(Exception): # pragma: no cover
|
|
345
|
+
'''
|
|
346
|
+
Raises an error identifying the missing DICOM tag.
|
|
347
|
+
'''
|
|
348
|
+
def __init__(self, tag_name):
|
|
349
|
+
self.tag_name = tag_name
|
|
350
|
+
super().__init__(f"Tag '{tag_name}' is missing.")
|
|
351
|
+
|
|
352
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
353
|
+
class NotCalledError(Exception): # pragma: no cover
|
|
354
|
+
"""
|
|
355
|
+
Exception raised when a required __call__ method has not been used.
|
|
356
|
+
"""
|
|
357
|
+
def __init__(self, message="__call__ method has not been invoked."):
|
|
358
|
+
self.message = message
|
|
359
|
+
super().__init__(self.message)
|
|
360
|
+
|
|
361
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
362
|
+
class FileValidationError(Exception): # pragma: no cover
|
|
363
|
+
'''
|
|
364
|
+
General exception raised when a file was not valid
|
|
365
|
+
'''
|
|
366
|
+
def __init__(self, message="File failed to validate."):
|
|
367
|
+
self.message = message
|
|
368
|
+
super().__init__(self.message)
|
|
369
|
+
|
|
370
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
371
|
+
class XMLValidationError(FileValidationError):
|
|
372
|
+
"""
|
|
373
|
+
Exception raised when the XML and XSD file are incompatible.
|
|
374
|
+
"""
|
|
375
|
+
def __init__(self, errors: list[str]):
|
|
376
|
+
custom_message = (
|
|
377
|
+
"XML validation failed due to the following issues:\n"
|
|
378
|
+
+ "\n".join(errors)
|
|
379
|
+
)
|
|
380
|
+
super().__init__(custom_message)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|