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.
Files changed (33) hide show
  1. ecgprocess/__init__.py +1 -0
  2. ecgprocess/_version.py +1 -0
  3. ecgprocess/constants.py +170 -0
  4. ecgprocess/errors.py +380 -0
  5. ecgprocess/example_data/__init__.py +1 -0
  6. ecgprocess/example_data/example_datasets/.example-XML1.xml.un~ +0 -0
  7. ecgprocess/example_data/example_datasets/.example-XML1.xsd.un~ +0 -0
  8. ecgprocess/example_data/example_datasets/.example-XML1_a.xsd.un~ +0 -0
  9. ecgprocess/example_data/example_datasets/.example-XML1_d.xsd.un~ +0 -0
  10. ecgprocess/example_data/example_datasets/.example-XML1_wrong.xsd.un~ +0 -0
  11. ecgprocess/example_data/example_datasets/example-DICOM1.dcm +0 -0
  12. ecgprocess/example_data/example_datasets/example-DICOM2.dcm +0 -0
  13. ecgprocess/example_data/example_datasets/example-XML1.xml +675 -0
  14. ecgprocess/example_data/example_datasets/example-XML1.xsd +518 -0
  15. ecgprocess/example_data/example_datasets/example-XML1_error.xsd +519 -0
  16. ecgprocess/example_data/example_datasets/example-XML1_wrong.xsd +519 -0
  17. ecgprocess/example_data/example_datasets/string_data.txt +1 -0
  18. ecgprocess/example_data/examples.py +501 -0
  19. ecgprocess/plot_ecgs.py +921 -0
  20. ecgprocess/process_dicom.py +243 -0
  21. ecgprocess/process_xml.py +319 -0
  22. ecgprocess/tabular.py +1394 -0
  23. ecgprocess/utils/__init__.py +1 -0
  24. ecgprocess/utils/config_tools.py +581 -0
  25. ecgprocess/utils/ecg_tools.py +311 -0
  26. ecgprocess/utils/engineering_tools.py +385 -0
  27. ecgprocess/utils/general.py +399 -0
  28. ecgprocess/utils/reader_tools.py +508 -0
  29. ecgprocess-1.1.0.dist-info/METADATA +153 -0
  30. ecgprocess-1.1.0.dist-info/RECORD +33 -0
  31. ecgprocess-1.1.0.dist-info/WHEEL +5 -0
  32. ecgprocess-1.1.0.dist-info/licenses/LICENCE.txt +17 -0
  33. 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'
@@ -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
+