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
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,581 @@
1
+ import re
2
+ import warnings
3
+ from dataclasses import dataclass, field
4
+ from typing import Optional, Any, Self, Literal
5
+ from ecgprocess.constants import (
6
+ CoreData as Core,
7
+ UtilsConfigData as ConfigNames,
8
+ )
9
+ from ecgprocess.errors import (
10
+ InputValidationError,
11
+ is_type,
12
+ Error_MSG,
13
+ NotCalledError,
14
+ _check_is_file,
15
+ )
16
+ from ecgprocess.utils.general import ManagedProperty
17
+
18
+ """
19
+ Classes to handle and parse configuration files describing how the XML/DICOM
20
+ tags should be assigned to dataclass attributes.
21
+ """
22
+
23
+ # @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
24
+ # constants
25
+ CTypes = Core.DataTypes
26
+ CLeads = Core.Leads
27
+ CMeta = Core.MetaData
28
+
29
+ # @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
30
+ @dataclass
31
+ class PrivilegedData(object):
32
+ '''
33
+ The core metadata dictionary, where the key values cannot not be changed
34
+ because these are expected used by downstream programs. The values can
35
+ however be user defined.
36
+
37
+ Methods
38
+ -------
39
+ update_values(**kwargs)
40
+ Update the values of existing dictionary keys.
41
+ to_dict()
42
+ Returns the current dictionary.
43
+ keys()
44
+ Returns the dictionary keys.
45
+
46
+ Notes
47
+ -----
48
+ The values are initialised to `NoneType` these should be set to values
49
+ relevant for the relevant XML/DICOM tags/attributes.
50
+
51
+ Examples
52
+ --------
53
+ >>> required = PrivilegedData()
54
+ >>> print(*required.keys(), sep='\n')
55
+ unique identifier
56
+ sampling frequency (original)
57
+ sampling number (waveforms)
58
+ sampling number (medianbeats)
59
+ acquisition date
60
+ study date
61
+ channel number
62
+ units (waveforms)
63
+ units (medianbeats)
64
+ '''
65
+ # Defaults, using default_factory and lambda to make sure default do not
66
+ # persist between instances.
67
+ # NOTE using a single _ instead of __ to prevent name mangling.
68
+ _data: dict[str, str] = field(default_factory=lambda: {
69
+ CMeta.UID : None,
70
+ CMeta.NLEADS : None,
71
+ CMeta.RES_U_W : None,
72
+ CMeta.RES_U_M : None,
73
+ CMeta.RES_W : None,
74
+ CMeta.RES_M : None,
75
+ CMeta.SF : None,
76
+ CMeta.SF_U : None,
77
+ CMeta.SN_W : None,
78
+ CMeta.SN_M : None,
79
+ })
80
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
81
+ def update_values(self, **kwargs:Optional[Any]) -> None:
82
+ """
83
+ Update the values of existing immutable keys.
84
+
85
+ Parameters
86
+ ----------
87
+ **kwargs : `dict` [`str`, `str`]
88
+ Key-value pairs where the key is a required data field and the
89
+ value is the updated DICOM/XML tag.
90
+
91
+ Raises
92
+ ------
93
+ KeyError
94
+ If a key in `kwargs` is not a valid required data field.
95
+ """
96
+ CLASS_NAME = type(self).__name__
97
+ for key, val in kwargs.items():
98
+ if key not in self._data:
99
+ raise KeyError(f"Key '{key}' is not a valid attribute for "
100
+ f"{CLASS_NAME}.")
101
+ self._data[key] = val
102
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
103
+ def keys(self) -> list[str]:
104
+ """
105
+ Returns the required keys.
106
+ """
107
+ return list(self._data.keys())
108
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
109
+ def to_dict(self) -> dict[str, str]:
110
+ """
111
+ Returns the required data as a dictionary.
112
+ """
113
+ return self._data
114
+
115
+ # @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
116
+ @dataclass
117
+ class OtherData(PrivilegedData):
118
+ """
119
+ A class to map internal attribute names to DICOM/XML tags and collect data
120
+ based on optional attributes.
121
+
122
+ Methods
123
+ -------
124
+ update_values(**kwargs)
125
+ Update the values of existing dictionary keys.
126
+ to_dict()
127
+ Returns the current dictionary.
128
+ keys()
129
+ Returns the dictionary keys.
130
+ """
131
+ # overwrite _data from PrivilegedData
132
+ _data: Optional[dict[str, str]] = field(default_factory=dict)
133
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
134
+ def update_values(self, **kwargs:Optional[Any]) -> None:
135
+ """
136
+ Update the dictionary key and values.
137
+
138
+ Parameters
139
+ ----------
140
+ **kwargs : `dict` [`str`, `str`]
141
+ Key-value pairs mapping DICOM/XML tags (values) to attribute names
142
+ (keys).
143
+
144
+ """
145
+ for key, val in kwargs.items():
146
+ self._data[key] = val
147
+
148
+ # @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
149
+ @dataclass
150
+ class DataMap(object):
151
+ """
152
+ A class to manage and map metadata, waveforms, and median beats,
153
+ and optionally, other data attributes.
154
+
155
+ Attributes
156
+ ----------
157
+ WaveForms : `dict` [`str`, `str`]
158
+ A config dict mapping the twelve ECG leads.
159
+ MedianBeats : `dict` [`str`, `str`]
160
+ A config dict mapping the twelve ECG leads.
161
+ MetaData : `dict` [`str`, `str`]
162
+ A dictionary containing metadata attributes and their values.
163
+ OtherData : `dict` [`str`, `str`], default `NoneType`
164
+ An optional dictionary for additional data mappings.
165
+
166
+ Methods
167
+ -------
168
+ get_attributes(all_)
169
+ Returns the names of attributes with non-None values.
170
+ keys(attr_name)
171
+ Returns the keys of the specified dictionary ('MetaData' or
172
+ 'OtherData'), or both if no specific dictionary is specified.
173
+ items(attr_name)
174
+ Returns the key-value pairs of the specified dictionary ('MetaData' or
175
+ 'OtherData'), or both if no specific dictionary is specified.
176
+
177
+ Examples
178
+ --------
179
+ >>> data_map = DataMap()
180
+ >>> data_map.get_attributes()
181
+ ['WaveForms', 'MedianBeats', 'MetaData']
182
+
183
+ Notes
184
+ -----
185
+ The class is intended to work in conjunction with a config file processed
186
+ using `ConfigParser.map(DataMap)`. DataMap ensures any privileged
187
+ attributes omitted from the config are added and set to None, and prevents
188
+ the config file to be altered after processing.
189
+ """
190
+ # setting attributes
191
+ WaveForms: dict = field(default_factory=lambda: PrivilegedData(
192
+ _data=DataMap.get_leads()).to_dict())
193
+ MedianBeats: dict = field(default_factory=lambda: PrivilegedData(
194
+ _data=DataMap.get_leads()).to_dict())
195
+ MetaData: dict = field(default_factory=lambda: PrivilegedData().to_dict())
196
+ OtherData: dict | None = None
197
+ _VALIDATE_ATTR = (
198
+ ConfigNames.WaveForms, ConfigNames.MedianBeats,
199
+ ConfigNames.MetaData, ConfigNames.OtherData
200
+ )
201
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
202
+ @staticmethod
203
+ def get_leads():
204
+ """Initialising the leads names to None"""
205
+ lead_attr = [attr for attr in dir(CLeads) if not attr.startswith('__')]
206
+ return {getattr(CLeads, l): None for l in lead_attr}
207
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
208
+ def __post_init__(self):
209
+ """Validating inputs."""
210
+ is_type(self.WaveForms, dict, 'WaveForms')
211
+ is_type(self.MedianBeats, dict, 'MedianBeats')
212
+ is_type(self.MetaData, dict, 'MetaData')
213
+ is_type(self.OtherData, (type(None), dict), 'OtherData')
214
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
215
+ @property
216
+ def VALIDATE_ATTR(self) -> tuple:
217
+ """
218
+ Exposes _VALIDATE_ATTR as a read-only property.
219
+ """
220
+ return self._VALIDATE_ATTR
221
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
222
+ def get_attributes(self, all_:bool=False) -> list[str]:
223
+ """
224
+ Returns the names of attributes with non-None values.
225
+
226
+ Attributes
227
+ ---------
228
+ all_ : `bool`, default `False`
229
+ Whether to return all keys irrespective of whether their values are
230
+ equal to None.
231
+ Returns
232
+ -------
233
+ list [`str`]
234
+ A list of attribute names where the value is not None.
235
+ """
236
+ is_type(all_, bool)
237
+ if all_ == True:
238
+ res = list(self.__dict__.keys())
239
+ else:
240
+ res = [at for at, val in self.__dict__.items() if val is not None]
241
+ return res
242
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
243
+ def keys(self, attr_name: str | None = None,) -> list[str]:
244
+ """
245
+ Return the keys for WaveForms, MedianBeats, MetaData, OtherData, or
246
+ all dictionaries.
247
+
248
+ Parameters
249
+ ----------
250
+ attr_name : `str`, default `NoneType`
251
+ Specify attribute name to limit the result to that dictionary. If
252
+ None, keys for all dictionaries are returned, without attempting
253
+ to unique these.
254
+
255
+ Returns
256
+ -------
257
+ `list` [`str`]
258
+ A list with dictionary keys.
259
+ """
260
+ # #### check input
261
+ is_type(attr_name, (type(None), str))
262
+ if not attr_name in self._VALIDATE_ATTR and not attr_name is None:
263
+ raise ValueError(Error_MSG.CHOICE_PARM.\
264
+ format('attr_name', ', '.join(self._VALIDATE_ATTR)))
265
+ # find keys
266
+ attributes = self._VALIDATE_ATTR if attr_name is None else [attr_name]
267
+ result = []
268
+ for attr in attributes:
269
+ attribute = getattr(self, attr)
270
+ try:
271
+ result = result + list(attribute.keys())
272
+ except AttributeError:
273
+ pass
274
+ return result
275
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
276
+ def items(self, attr_name: str|None = None) -> dict[str, dict[str, str]]:
277
+ """
278
+ Return the key-value pairs forWaveForms, MedianBeats, MetaData,
279
+ OtherData, or all dictionaries.
280
+
281
+ Parameters
282
+ ----------
283
+ attr_name : `str`, default `NoneType`
284
+ Specify the type of attribute to limit the result to that
285
+ dictionary. If None, items for all dictionaries are returned.
286
+
287
+ Returns
288
+ -------
289
+ `dict` [`str`, `dict` [`str`, `str`]
290
+ A dictionary where keys are attribute names and values are the
291
+ key-value pairs of the dictionary.
292
+ """
293
+ # #### check input
294
+ is_type(attr_name, (type(None), str))
295
+ if not attr_name in self._VALIDATE_ATTR and not attr_name is None:
296
+ raise ValueError(Error_MSG.CHOICE_PARM.\
297
+ format('attr_name', ', '.join(self._VALIDATE_ATTR)))
298
+ # find keys
299
+ attributes = self._VALIDATE_ATTR if attr_name is None else [attr_name]
300
+ result = {attr:{} for attr in attributes}
301
+ for attr in attributes:
302
+ attr_sub = getattr(self, attr, None)
303
+ try:
304
+ result[attr].update(attr_sub.items())
305
+ except (AttributeError, TypeError):
306
+ pass
307
+ return result
308
+
309
+ # @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
310
+ class ConfigParser(object):
311
+ """
312
+ Parses configuration files into structured data and optionally assigns this
313
+ to a user supplied mapper instance.
314
+
315
+ Parameters
316
+ ----------
317
+ path : str
318
+ Path to the configuration file to be parsed.
319
+
320
+ Notes
321
+ -----
322
+ Required call sequence::
323
+
324
+ parser = ConfigParser(path)() # parse the file
325
+ parser.map(mapper) # map sections to a DataMap
326
+ parser.get_section(name) # retrieve a section
327
+
328
+ Calling `get_section()` before `map()` raises `NotCalledError`.
329
+ """
330
+ # properties
331
+ path = ManagedProperty(ConfigNames.path)
332
+ _data = ManagedProperty(ConfigNames.data, dict)
333
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
334
+ def __init__(self, path:str):
335
+ '''
336
+ Initialize the ConfigParser instance.
337
+ '''
338
+ # making sure the setter is only used during _init_
339
+ getattr(type(self),ConfigNames.path).set_with_setter(self, path)
340
+ getattr(type(self),ConfigNames.data).set_with_setter(self, {})
341
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
342
+ def __eq__(self, other):
343
+ """
344
+ Determine how instances are compared.
345
+ """
346
+ if len(getattr(self, ConfigNames.data)) == 0:
347
+ if self.path == other.path and self._data == other._data:
348
+ return True
349
+ elif hasattr(self, ConfigNames.MAPPER) and\
350
+ hasattr(other, ConfigNames.MAPPER):
351
+ if getattr(self, ConfigNames.MAPPER) ==\
352
+ getattr(other, ConfigNames.MAPPER):
353
+ return True
354
+ elif self._data == other._data:
355
+ return True
356
+ return False
357
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
358
+ def __call__(self) -> Self:
359
+ """
360
+ Parse the configuration file.
361
+
362
+ This method reads the configuration file specified during
363
+ initialization, validates its structure, and stores the data in a
364
+ dictionary. It supports sections with key-value pairs.
365
+
366
+ Raises
367
+ ------
368
+ FileNotFoundError
369
+ If the configuration file does not exist.
370
+ IsADirectoryError
371
+ If the configuration path exists but is not a regular file.
372
+ ValueError
373
+ If a parsed line does not contain a tab delimiter.
374
+ InputValidationError
375
+ If a value-key pair is duplicated within the same section.
376
+ """
377
+ current = None
378
+ path = getattr(self, ConfigNames.path)
379
+ _check_is_file(path)
380
+ with open(path, "r", encoding="utf-8") as file:
381
+ for line in file:
382
+ line = line.strip()
383
+ if line.startswith("[") and line.endswith("]"):
384
+ # Start a new section
385
+ current = line.strip("[]")
386
+ # check if the attribute type is allowed
387
+ if hasattr(ConfigNames, current) == False:
388
+ raise AttributeError(
389
+ f"Encountered an unknown data type or malformed "
390
+ f"input: `{current}`. Ensure the file format is "
391
+ "correct and all sections are properly defined."
392
+ )
393
+ # confirm this is not already in data - fine if it is,
394
+ # no need to create a new key and subdict.
395
+ # can just add new entries.
396
+ if current not in getattr(self, ConfigNames.data):
397
+ getattr(self, ConfigNames.data)[current] = {}
398
+ # skip if there is a comment
399
+ elif re.match(r"^\s*#", line):
400
+ continue
401
+ elif current and line:
402
+ # Process the line
403
+ if "\t" in line:
404
+ # Split into key-value pairs
405
+ k, v = re.split(r'\t+', line, maxsplit=1)
406
+ # include a strip to make sure spaces are trimmed
407
+ if not k.strip() in\
408
+ getattr(self, ConfigNames.data)[current]:
409
+ getattr(self, ConfigNames.data)[current][k.strip()]=\
410
+ v.strip()
411
+ else:
412
+ raise InputValidationError(
413
+ f"`{k.strip()}` has already been set: "
414
+ f"{getattr(self, ConfigNames.data)[current]}. "
415
+ "Please ensure a single unique name has been "
416
+ "supplied for each variable."
417
+ )
418
+ else:
419
+ raise ValueError("Missing tab delimiter.")
420
+ return self
421
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
422
+ def map(self, mapper:DataMap) -> None:
423
+ """
424
+ Maps the parsed data to a supplied `DataMap` instance.
425
+
426
+ Attributes
427
+ ----------
428
+ mapper : `DataMap`
429
+ An instance of the `DataMap` class, which is expected to have
430
+ attributes corresponding to the parsed data sections (e.g., headers
431
+ or types). These attributes will be updated with the data parsed by
432
+ this class.
433
+
434
+ Notes
435
+ -----
436
+ The method does not return anything and simply creates the `mapper`
437
+ attribute.
438
+ """
439
+ WRONG = ('Attribute `{}` is outside the allowed for key set for `{}`.'
440
+ 'Please update your configuration file to only list allowed '
441
+ 'attribute names. For example, consult the mapper object.')
442
+ # ### make sure the instance was called
443
+ if len(getattr(self, ConfigNames.data)) == 0:
444
+ raise NotCalledError
445
+ # ### check input
446
+ missing = [a for a in getattr(self, ConfigNames.data).keys() if\
447
+ not hasattr(mapper, a)]
448
+ if len(missing) > 0:
449
+ raise AttributeError(
450
+ f"The following parsed data types are not defined as "
451
+ f"attributes in the provided mapper instance: "
452
+ f"{', '.join(missing)}. Please ensure the mapper has "
453
+ f"corresponding attributes for all required data types."
454
+ )
455
+ # ### class instances
456
+ # setting up the signal data
457
+ lead_keys = mapper.get_leads()
458
+ wave_data = PrivilegedData(_data=lead_keys.copy())
459
+ median_data = PrivilegedData(_data=lead_keys.copy())
460
+ lead_keys = lead_keys.keys()
461
+ # setting up the meta/other data
462
+ meta_data_req = PrivilegedData()
463
+ req_keys = meta_data_req.keys()
464
+ meta_data_other = OtherData()
465
+ other_data = OtherData()
466
+ for key, d in getattr(self, ConfigNames.data).items():
467
+ for k, v in d.items():
468
+ if key == ConfigNames.MedianBeats:
469
+ if k in lead_keys:
470
+ median_data.update_values(**{k:v})
471
+ else:
472
+ raise KeyError(WRONG.format(k, key))
473
+ elif key == ConfigNames.WaveForms:
474
+ if k in lead_keys:
475
+ wave_data.update_values(**{k:v})
476
+ else:
477
+ raise KeyError(WRONG.format(k, key))
478
+ elif key == ConfigNames.MetaData:
479
+ # identify the required keys
480
+ if k in req_keys:
481
+ meta_data_req.update_values(**{k:v})
482
+ else:
483
+ meta_data_other.update_values(**{k:v})
484
+ else:
485
+ # the non-meta data
486
+ other_data.update_values(**{k:v})
487
+ # #### set attributes
488
+ # set meta-data and other data
489
+ setattr(mapper, ConfigNames.OtherData, other_data.to_dict())
490
+ setattr(mapper, ConfigNames.WaveForms, wave_data.to_dict())
491
+ setattr(mapper, ConfigNames.MedianBeats, median_data.to_dict())
492
+ setattr(mapper, ConfigNames.MetaData,
493
+ {**meta_data_req.to_dict(),
494
+ **meta_data_other.to_dict()}
495
+ )
496
+ setattr(self, ConfigNames.MAPPER, mapper)
497
+ # # return stuff
498
+ # return mapper
499
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
500
+ def get_section(self,
501
+ section_name:Literal['MetaData', 'WaveForms', 'MedianBeats',
502
+ 'OtherData'],
503
+ ) -> dict[str, str]:
504
+ """
505
+ Retrieve the parsed data for a specific section of the configuration
506
+ file.
507
+
508
+ Parameters
509
+ ----------
510
+ section_name : {'MetaData', 'WaveForms', 'MedianBeats', 'OtherData'}
511
+ Name of the section to retrieve.
512
+
513
+ Returns
514
+ -------
515
+ section : `dict` [`str`, `str`]
516
+ A dictionary of key-value pairs for the requested section.
517
+
518
+ Raises
519
+ ------
520
+ NotCalledError
521
+ If `map()` has not been called before this method.
522
+ ValueError
523
+ If `section_name` is not one of the allowed section names.
524
+ KeyError
525
+ If the requested section has no data.
526
+ """
527
+ is_type(section_name, str)
528
+ SECTION_NAME = [ConfigNames.OtherData, ConfigNames.WaveForms,
529
+ ConfigNames.MedianBeats, ConfigNames.MetaData]
530
+ # confirm the choice is correct
531
+ if not section_name in SECTION_NAME:
532
+ raise ValueError(Error_MSG.CHOICE_PARM.\
533
+ format('section_name', ', '.join(SECTION_NAME)))
534
+ # confirm mapper has been run
535
+ if not hasattr(self, ConfigNames.MAPPER):
536
+ raise NotCalledError('`map()` must be called before `get_section()`.')
537
+ # get the correct attribute
538
+ res = getattr(getattr(self, ConfigNames.MAPPER), section_name)
539
+ if res is None:
540
+ raise KeyError(f"key {section_name} is not available.")
541
+ return res
542
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
543
+ def __str__(self):
544
+ """String representation of the parsed data."""
545
+ result = [f"{self.__class__.__name__}"]
546
+ if not getattr(self, ConfigNames.data):
547
+ j = ""
548
+ result.append(f"path={getattr(self,ConfigNames.path)}")
549
+ else:
550
+ j = "\n"
551
+ # Determine the maximum key length for alignment
552
+ max_key_length = max(
553
+ (len(key) for _, attr in getattr(self, ConfigNames.data).items()
554
+ for key in attr.keys()), default=0)
555
+ for section, attributes in getattr(self, ConfigNames.data).items():
556
+ result.append(f"[{section}]")
557
+ for key, value in attributes.items():
558
+ # Align values by padding keys
559
+ result.append(f"\t{key:<{max_key_length}} {value}")
560
+ result.append("")
561
+ return j.join(result)
562
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
563
+ def __repr__(self):
564
+ """Developer-friendly representation of the parsed data."""
565
+ result = [f"{self.__class__.__name__}("]
566
+ if not getattr(self, ConfigNames.data):
567
+ j = ""
568
+ result.append(f"path={getattr(self, ConfigNames.path)}")
569
+ else:
570
+ j = "\n"
571
+ # Determine the maximum key length for alignment
572
+ max_key_length = max(
573
+ (len(key) for _, attr in getattr(self, ConfigNames.data).items()
574
+ for key in attr.keys()), default=0)
575
+ for section, attributes in getattr(self, ConfigNames.data).items():
576
+ result.append(f" [{section}]")
577
+ for key, value in attributes.items():
578
+ # Align values by padding keys
579
+ result.append(f"\t{key:<{max_key_length}} {value}")
580
+ result.append(")")
581
+ return j.join(result)