spectre-core 0.0.12__py3-none-any.whl → 0.0.14__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 (88) hide show
  1. spectre_core/_file_io/__init__.py +1 -3
  2. spectre_core/_file_io/file_handlers.py +163 -58
  3. spectre_core/batches/__init__.py +10 -11
  4. spectre_core/batches/_base.py +170 -78
  5. spectre_core/batches/_batches.py +149 -99
  6. spectre_core/batches/_factory.py +56 -14
  7. spectre_core/batches/_register.py +23 -8
  8. spectre_core/batches/plugins/_batch_keys.py +16 -0
  9. spectre_core/batches/plugins/_callisto.py +183 -0
  10. spectre_core/batches/plugins/_iq_stream.py +354 -0
  11. spectre_core/capture_configs/__init__.py +17 -13
  12. spectre_core/capture_configs/_capture_config.py +93 -34
  13. spectre_core/capture_configs/_capture_modes.py +22 -0
  14. spectre_core/capture_configs/_capture_templates.py +207 -122
  15. spectre_core/capture_configs/_parameters.py +115 -42
  16. spectre_core/capture_configs/_pconstraints.py +86 -35
  17. spectre_core/capture_configs/_pnames.py +49 -0
  18. spectre_core/capture_configs/_ptemplates.py +389 -346
  19. spectre_core/capture_configs/_pvalidators.py +117 -73
  20. spectre_core/config/__init__.py +6 -8
  21. spectre_core/config/_paths.py +65 -25
  22. spectre_core/config/_time_formats.py +15 -10
  23. spectre_core/exceptions.py +2 -4
  24. spectre_core/jobs/__init__.py +14 -0
  25. spectre_core/jobs/_jobs.py +111 -0
  26. spectre_core/jobs/_workers.py +171 -0
  27. spectre_core/logs/__init__.py +17 -0
  28. spectre_core/logs/_configure.py +67 -0
  29. spectre_core/logs/_decorators.py +33 -0
  30. spectre_core/logs/_logs.py +228 -0
  31. spectre_core/logs/_process_types.py +14 -0
  32. spectre_core/plotting/__init__.py +4 -2
  33. spectre_core/plotting/_base.py +204 -102
  34. spectre_core/plotting/_format.py +17 -4
  35. spectre_core/plotting/_panel_names.py +18 -0
  36. spectre_core/plotting/_panel_stack.py +167 -53
  37. spectre_core/plotting/_panels.py +341 -141
  38. spectre_core/post_processing/__init__.py +8 -6
  39. spectre_core/post_processing/_base.py +70 -44
  40. spectre_core/post_processing/_factory.py +42 -12
  41. spectre_core/post_processing/_post_processor.py +24 -26
  42. spectre_core/post_processing/_register.py +22 -6
  43. spectre_core/post_processing/plugins/_event_handler_keys.py +16 -0
  44. spectre_core/post_processing/plugins/_fixed_center_frequency.py +129 -0
  45. spectre_core/post_processing/{library → plugins}/_swept_center_frequency.py +215 -143
  46. spectre_core/py.typed +0 -0
  47. spectre_core/receivers/__init__.py +10 -7
  48. spectre_core/receivers/_base.py +220 -69
  49. spectre_core/receivers/_factory.py +53 -7
  50. spectre_core/receivers/_register.py +30 -9
  51. spectre_core/receivers/_spec_names.py +26 -15
  52. spectre_core/receivers/plugins/__init__.py +0 -0
  53. spectre_core/receivers/plugins/_receiver_names.py +16 -0
  54. spectre_core/receivers/plugins/_rsp1a.py +59 -0
  55. spectre_core/receivers/plugins/_rspduo.py +67 -0
  56. spectre_core/receivers/plugins/_sdrplay_receiver.py +190 -0
  57. spectre_core/receivers/plugins/_test.py +218 -0
  58. spectre_core/receivers/plugins/gr/_base.py +80 -0
  59. spectre_core/receivers/{gr → plugins/gr}/_rsp1a.py +42 -52
  60. spectre_core/receivers/{gr → plugins/gr}/_rspduo.py +61 -74
  61. spectre_core/receivers/{gr → plugins/gr}/_test.py +33 -31
  62. spectre_core/spectrograms/__init__.py +5 -3
  63. spectre_core/spectrograms/_analytical.py +121 -66
  64. spectre_core/spectrograms/_array_operations.py +103 -36
  65. spectre_core/spectrograms/_spectrogram.py +380 -207
  66. spectre_core/spectrograms/_transform.py +197 -169
  67. spectre_core/wgetting/__init__.py +4 -2
  68. spectre_core/wgetting/_callisto.py +173 -118
  69. {spectre_core-0.0.12.dist-info → spectre_core-0.0.14.dist-info}/METADATA +14 -7
  70. spectre_core-0.0.14.dist-info/RECORD +75 -0
  71. {spectre_core-0.0.12.dist-info → spectre_core-0.0.14.dist-info}/WHEEL +1 -1
  72. spectre_core/batches/library/_callisto.py +0 -96
  73. spectre_core/batches/library/_fixed_center_frequency.py +0 -133
  74. spectre_core/batches/library/_swept_center_frequency.py +0 -105
  75. spectre_core/logging/__init__.py +0 -11
  76. spectre_core/logging/_configure.py +0 -35
  77. spectre_core/logging/_decorators.py +0 -19
  78. spectre_core/logging/_log_handlers.py +0 -176
  79. spectre_core/post_processing/library/_fixed_center_frequency.py +0 -114
  80. spectre_core/receivers/gr/_base.py +0 -33
  81. spectre_core/receivers/library/_rsp1a.py +0 -61
  82. spectre_core/receivers/library/_rspduo.py +0 -69
  83. spectre_core/receivers/library/_sdrplay_receiver.py +0 -185
  84. spectre_core/receivers/library/_test.py +0 -221
  85. spectre_core-0.0.12.dist-info/RECORD +0 -64
  86. /spectre_core/receivers/{gr → plugins/gr}/__init__.py +0 -0
  87. {spectre_core-0.0.12.dist-info → spectre_core-0.0.14.dist-info}/LICENSE +0 -0
  88. {spectre_core-0.0.12.dist-info → spectre_core-0.0.14.dist-info}/top_level.txt +0 -0
@@ -2,9 +2,7 @@
2
2
  # This file is part of SPECTRE
3
3
  # SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
- """
6
- Internal file handling.
7
- """
5
+ """Basic internal file handling capabilities."""
8
6
 
9
7
  from .file_handlers import (
10
8
  BaseFileHandler, JsonHandler, TextHandler,
@@ -5,124 +5,229 @@
5
5
  import os
6
6
  import json
7
7
  from abc import ABC, abstractmethod
8
- from typing import Any, Optional
9
-
10
-
11
- class BaseFileHandler(ABC):
12
- def __init__(self,
13
- parent_dir_path: str,
14
- base_file_name: str,
15
- extension: Optional[str] = None):
8
+ from typing import Any, Optional, TypeVar, Generic
9
+
10
+ T = TypeVar('T')
11
+
12
+ class BaseFileHandler(ABC, Generic[T]):
13
+ """
14
+ Base class for handling file operations.
15
+
16
+ When subclassing, specify the return type of `_read`
17
+ using `Generic[T]`.
18
+
19
+ Example:
20
+ .. code-block:: python
21
+
22
+ from typing import Any, Generic
23
+
24
+ class JsonHandler(BaseFileHandler[dict[str, Any]]):
25
+ def _read(self) -> dict[str, Any]:
26
+ # Implementation here
27
+ ...
28
+ """
29
+ def __init__(
30
+ self,
31
+ parent_dir_path: str,
32
+ base_file_name: str,
33
+ extension: Optional[str] = None
34
+ ) -> None:
35
+ """Initialise a `BaseFileHandler` instance.
36
+
37
+ :param parent_dir_path: The directory path where the file is located (absolute or relative).
38
+ :param base_file_name: The name of the file without its extension.
39
+ :param extension: The file extension (without the dot), defaults to None
40
+ """
41
+ self._data_cache: Optional[T] = None
42
+
16
43
  self._parent_dir_path = parent_dir_path
17
- self._base_file_name = base_file_name
44
+ self._base_file_name = base_file_name
45
+
46
+ if extension == "":
47
+ extension = None
18
48
  self._extension = extension
19
49
 
20
-
50
+
21
51
  @abstractmethod
22
- def read(self) -> Any:
23
- pass
24
-
25
-
52
+ def _read(
53
+ self
54
+ ) -> T:
55
+ """The grunt work to return the file contents.
56
+
57
+ :return: The file contents.
58
+ """
59
+
60
+
26
61
  @property
27
- def parent_dir_path(self) -> str:
62
+ def parent_dir_path(
63
+ self
64
+ ) -> str:
65
+ """Return the parent directory path for the file."""
28
66
  return self._parent_dir_path
29
67
 
30
68
 
31
69
  @property
32
- def base_file_name(self) -> str:
70
+ def base_file_name(
71
+ self
72
+ ) -> str:
73
+ """Return the file name, stripped of the file extension."""
33
74
  return self._base_file_name
34
75
 
35
76
 
36
77
  @property
37
- def extension(self) -> Optional[str]:
78
+ def extension(
79
+ self
80
+ ) -> Optional[str]:
81
+ """Return the file path suffix, excluding the dot."""
38
82
  return self._extension
39
83
 
40
84
 
41
85
  @property
42
- def file_name(self) -> str:
86
+ def file_name(
87
+ self
88
+ ) -> str:
89
+ """Generate the file name based on the base name and extension.
90
+
91
+ :return: The file name with the extension (including the dot), or the base name if no extension is set.
92
+ """
43
93
  return self._base_file_name if (self._extension is None) else f"{self._base_file_name}.{self._extension}"
44
94
 
45
95
 
46
96
  @property
47
- def file_path(self) -> str:
97
+ def file_path(
98
+ self
99
+ ) -> str:
100
+ """The absolute or relative file path as defined by the parent directory path,
101
+ base file name and extension."""
48
102
  return os.path.join(self._parent_dir_path, self.file_name)
49
103
 
50
104
 
51
105
  @property
52
- def exists(self) -> bool:
106
+ def exists(
107
+ self
108
+ ) -> bool:
109
+ """Check if the file exists in the filesystem."""
53
110
  return os.path.exists(self.file_path)
54
111
 
55
112
 
56
- def make_parent_dir_path(self) -> None:
113
+ def read(
114
+ self,
115
+ cache: bool = True
116
+ ) -> T:
117
+ """Read the file contents.
118
+
119
+ :param cache: If False, bypasses the cache and reads the file directly on each `read` call, defaults to True
120
+ :return: The file contents.
121
+ """
122
+ # if the user has specified to ignore the cache, simply read the file.
123
+ if not cache:
124
+ return self._read()
125
+
126
+ # otherwise make use of the cache
127
+ if self._data_cache is None:
128
+ self._data_cache = self._read()
129
+ return self._data_cache
130
+
131
+
132
+ def make_parent_dir_path(
133
+ self
134
+ ) -> None:
135
+ """Make the parent directory path of the file. No error is raised if the target
136
+ directory already exists.
137
+ """
57
138
  os.makedirs(self.parent_dir_path, exist_ok=True)
58
139
 
59
140
 
60
- def delete(self,
61
- ignore_if_missing: bool = False) -> None:
141
+ def delete(
142
+ self,
143
+ ignore_if_missing: bool = False
144
+ ) -> None:
145
+ """Delete the file from the filesystem.
146
+
147
+ :param ignore_if_missing: If True, skips deletion if the file does not exist, defaults to False
148
+ :raises FileNotFoundError: If the file is missing and `ignore_if_missing` is False.
149
+ """
62
150
  if not self.exists and not ignore_if_missing:
63
151
  raise FileNotFoundError(f"{self.file_name} does not exist, and so cannot be deleted")
64
152
  else:
65
153
  os.remove(self.file_path)
66
154
 
67
155
 
68
- def cat(self) -> None:
156
+ def cat(
157
+ self
158
+ ) -> None:
159
+ """Display the file contents on the standard output."""
69
160
  print(self.read())
70
161
 
71
162
 
72
- class JsonHandler(BaseFileHandler):
73
- def __init__(self,
74
- parent_dir_path: str,
75
- base_file_name: str,
76
- extension: str = "json",
77
- **kwargs):
78
-
79
- self._dict = None # cache
163
+ class JsonHandler(BaseFileHandler[dict[str, Any]]):
164
+ """File handler for JSON formatted files.
165
+
166
+ We assume that the files are of the form
167
+ {
168
+ "foo": <JSON compatable structure>
169
+ ... and so on.
170
+ }
171
+
172
+ """
173
+ def __init__(
174
+ self,
175
+ parent_dir_path: str,
176
+ base_file_name: str,
177
+ extension: str = "json"
178
+ ) -> None:
80
179
  super().__init__(parent_dir_path,
81
180
  base_file_name,
82
- extension,
83
- **kwargs)
181
+ extension)
84
182
 
85
183
 
86
- def read(self) -> dict[str, Any]:
184
+ def _read(
185
+ self
186
+ ) -> dict[str, Any]:
87
187
  with open(self.file_path, 'r') as f:
88
188
  return json.load(f)
89
189
 
90
-
91
- def save(self,
92
- d: dict,
93
- force: bool = False) -> None:
190
+
191
+ def save(
192
+ self,
193
+ d: dict[str, Any],
194
+ force: bool = False
195
+ ) -> None:
196
+ """Save the input dictionary to file in the JSON file format.
197
+
198
+ :param d: The dictionary to save.
199
+ :param force: If True, overwrites the file if it already exists, defaults to False
200
+ :raises FileExistsError: If the file exists and `force` is False.
201
+ """
94
202
  self.make_parent_dir_path()
95
203
 
96
204
  if self.exists:
97
205
  if force:
98
206
  pass
99
207
  else:
100
- raise RuntimeError((f"{self.file_name} already exists, write has been abandoned. "
101
- f"You can override this functionality with `force`"))
208
+ raise FileExistsError((f"{self.file_name} already exists, write has been abandoned. "
209
+ f"You can override this functionality with `force`"))
102
210
 
103
211
  with open(self.file_path, 'w') as file:
104
212
  json.dump(d, file, indent=4)
105
213
 
214
+
106
215
 
107
- @property
108
- def dict(self) -> dict[str, Any]:
109
- if self._dict is None:
110
- self._dict = self.read()
111
- return self._dict
112
-
113
-
114
- class TextHandler(BaseFileHandler):
115
- def __init__(self,
116
- parent_dir_path: str,
117
- base_file_name: str,
118
- extension: str = "txt",
119
- **kwargs):
216
+ class TextHandler(BaseFileHandler[str]):
217
+ """File handler for text formatted files."""
218
+ def __init__(
219
+ self,
220
+ parent_dir_path: str,
221
+ base_file_name: str,
222
+ extension: str = "txt"
223
+ ) -> None:
120
224
  super().__init__(parent_dir_path,
121
225
  base_file_name,
122
- extension,
123
- **kwargs)
226
+ extension)
124
227
 
125
-
126
- def read(self) -> dict:
228
+
229
+ def _read(
230
+ self
231
+ ) -> str:
127
232
  with open(self.file_path, 'r') as f:
128
233
  return f.read()
@@ -2,21 +2,20 @@
2
2
  # This file is part of SPECTRE
3
3
  # SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
- # register decorators are actioned on import
6
- from .library._fixed_center_frequency import _Batch
7
- from .library._swept_center_frequency import _Batch
8
- from .library._callisto import _Batch
5
+ """IO operations on batched data files."""
6
+
7
+ from .plugins._batch_keys import BatchKey
8
+
9
+ # register decorators take effect on import
10
+ from .plugins._iq_stream import IQStreamBatch, IQMetadata
11
+ from .plugins._callisto import CallistoBatch
9
12
 
10
13
  from ._base import BaseBatch, BatchFile
11
- from ._factory import get_batch_cls_from_tag
12
14
  from ._batches import Batches
13
- from .library._swept_center_frequency import SweepMetadata
15
+ from ._factory import get_batch_cls, get_batch_cls_from_tag
14
16
 
15
17
  __all__ = [
16
- "BaseBatch",
17
- "BatchFile",
18
- "get_batch_cls_from_tag",
19
- "Batches",
20
- "SweepMetadata"
18
+ "IQStreamBatch", "IQMetadata", "CallistoBatch", "BaseBatch", "BatchFile",
19
+ "Batches", "get_batch_cls", "BatchKey", "get_batch_cls_from_tag"
21
20
  ]
22
21
 
@@ -3,144 +3,236 @@
3
3
  # SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
5
  from datetime import datetime
6
- from typing import Optional
6
+ from typing import TypeVar
7
+ from functools import cached_property
8
+ from abc import ABC, abstractmethod
7
9
 
8
10
  from spectre_core._file_io import BaseFileHandler
9
- from spectre_core.config import get_batches_dir_path, TimeFormats
10
- from spectre_core.exceptions import BatchFileNotFoundError
11
+ from spectre_core.config import get_batches_dir_path, TimeFormat
12
+ from spectre_core.spectrograms import Spectrogram
11
13
 
12
14
 
13
- class BatchFile(BaseFileHandler):
14
- """A specific file in a given batch, uniquely identified in the batch by the file extension."""
15
- def __init__(self,
16
- batch_parent_dir_path: str,
17
- batch_name: str,
18
- extension: str):
15
+ T = TypeVar('T')
16
+
17
+ class BatchFile(BaseFileHandler[T]):
18
+ """Abstract base class for files belonging to a batch, identified by their file extension.
19
+
20
+ Batch file names must conform to the following structure:
21
+
22
+ `<start time>_<tag>.<extension>`
23
+
24
+ The substring `<start time>_<tag>` is referred to as the batch name. Files with the same batch name
25
+ belong to the same batch.
26
+ """
27
+ def __init__(
28
+ self,
29
+ batch_parent_dir_path: str,
30
+ batch_name: str,
31
+ extension: str
32
+ ) -> None:
33
+ """Initialise a `BatchFile` instance.
34
+
35
+ :param batch_parent_dir_path: Parent directory of the batch.
36
+ :param batch_name: Base file name, composed of the batch start time and tag.
37
+ :param extension: File extension.
38
+ """
19
39
  super().__init__(batch_parent_dir_path,
20
40
  batch_name,
21
41
  extension)
22
42
  self._start_time, self._tag = batch_name.split("_")
23
- # computed if required
24
- self._start_datetime: Optional[datetime] = None
25
-
26
-
43
+
44
+
27
45
  @property
28
- def start_time(self) -> str:
29
- """The start time of the batch file, up to seconds precision."""
46
+ def start_time(
47
+ self
48
+ ) -> str:
49
+ """The start time of the batch, formatted as a string up to seconds precision."""
30
50
  return self._start_time
31
51
 
32
52
 
33
- @property
34
- def start_datetime(self) -> datetime:
35
- """The datetime of the batch file, up to seconds precision."""
36
- if self._start_datetime is None:
37
- self._start_datetime = datetime.strptime(self.start_time, TimeFormats.DATETIME)
38
- return self._start_datetime
39
-
53
+ @cached_property
54
+ def start_datetime(
55
+ self
56
+ ) -> datetime:
57
+ """The start time of the batch, parsed as a datetime up to seconds precision."""
58
+ return datetime.strptime(self.start_time, TimeFormat.DATETIME)
40
59
 
60
+
41
61
  @property
42
- def tag(self) -> str:
43
- """The tag identifier for the batch file."""
62
+ def tag(
63
+ self
64
+ ) -> str:
65
+ """The batch name tag."""
44
66
  return self._tag
45
-
67
+
68
+
69
+ class BaseBatch(ABC):
70
+ """
71
+ An abstract base class representing a group of data files over a common time interval.
46
72
 
73
+ All files in a batch share a base file name and differ only by their extension.
74
+ Subclasses of `BaseBatch` define the expected data for each file extension and
75
+ provide an API for accessing their contents using `BatchFile` subclasses.
47
76
 
48
- class BaseBatch:
49
- """A group of one or more files which share a common start time and a tag identifier.
50
-
51
- All files belonging to the same batch will share a batch name, and differ
52
- only in their file extension.
77
+ Subclasses should expose `BatchFile` instances directly as attributes, which
78
+ simplifies static typing. Additionally, they should call `add_file` in the constructor
79
+ to formally register each `BatchFile`.
53
80
  """
54
- def __init__(self,
55
- start_time: str,
56
- tag: str):
81
+ def __init__(
82
+ self,
83
+ start_time: str,
84
+ tag: str
85
+ ) -> None:
86
+ """Initialise a `BaseBatch` instance.
87
+
88
+ :param start_time: Start time of the batch as a string with seconds precision.
89
+ :param tag: The batch name tag.
90
+ """
57
91
  self._start_time = start_time
58
92
  self._tag: str = tag
59
- self._batch_files: dict[str, BatchFile] = {}
60
- self._start_datetime = datetime.strptime(self.start_time, TimeFormats.DATETIME)
93
+ self._start_datetime = datetime.strptime(self.start_time, TimeFormat.DATETIME)
61
94
  self._parent_dir_path = get_batches_dir_path(year = self.start_datetime.year,
62
95
  month = self.start_datetime.month,
63
96
  day = self.start_datetime.day)
64
-
65
-
97
+
98
+ # internal register of batch files
99
+ self._batch_files: dict[str, BatchFile] = {}
100
+
101
+
102
+ @property
103
+ @abstractmethod
104
+ def spectrogram_file(
105
+ self
106
+ ) -> BatchFile:
107
+ """The batch file which contains spectrogram data."""
108
+
109
+
66
110
  @property
67
- def start_time(self) -> str:
68
- """The start time of the batch, up to seconds precision."""
111
+ def start_time(
112
+ self
113
+ ) -> str:
114
+ """The start time of the batch, formatted as a string up to seconds precision."""
69
115
  return self._start_time
70
116
 
71
117
 
72
118
  @property
73
- def tag(self) -> str:
74
- """The tag identifier of for the batch."""
75
- return self._tag
76
-
77
-
78
- @property
79
- def start_datetime(self) -> datetime:
80
- """The datetime of the batch file, up to seconds precision."""
119
+ def start_datetime(
120
+ self
121
+ ) -> datetime:
122
+ """The start time of the batch, parsed as a datetime up to seconds precision."""
81
123
  return self._start_datetime
82
124
 
125
+
126
+ @property
127
+ def tag(
128
+ self
129
+ ) -> str:
130
+ """The batch name tag."""
131
+ return self._tag
132
+
83
133
 
84
134
  @property
85
- def parent_dir_path(self) -> str:
135
+ def parent_dir_path(
136
+ self
137
+ ) -> str:
86
138
  """The parent directory for the batch."""
87
139
  return self._parent_dir_path
88
140
 
89
141
 
90
142
  @property
91
- def name(self) -> str:
92
- """The name of the batch."""
143
+ def name(
144
+ self
145
+ ) -> str:
146
+ """Return the base file name shared by all files in the batch,
147
+ composed of the start time and the batch tag."""
93
148
  return f"{self._start_time}_{self._tag}"
94
149
 
95
150
 
96
151
  @property
97
- def extensions(self) -> list[str]:
152
+ def extensions(
153
+ self
154
+ ) -> list[str]:
98
155
  """All defined file extensions for the batch."""
99
156
  return list(self._batch_files.keys())
100
157
 
158
+
101
159
  @property
102
- def batch_files(self) -> dict[str, BatchFile]:
103
- """Map each file extension in the batch to the corresponding batch file instance."""
160
+ def batch_files(
161
+ self
162
+ ) -> dict[str, BatchFile]:
163
+ """Map each file extension in the batch to the corresponding batch file instance.
164
+
165
+ Use `add_file` to add a file to the batch.
166
+ """
104
167
  return self._batch_files
105
168
 
106
-
107
- def add_file(self, batch_file: BatchFile) -> None:
108
- """Add an instance of a batch file to the batch."""
169
+
170
+ def add_file(
171
+ self,
172
+ batch_file: BatchFile
173
+ ) -> None:
174
+ """Add an instance of a batch file to the batch.
175
+
176
+ :param batch_file: The `BatchFile` instance to add to the batch.
177
+ :raises ValueError: If the `BatchFile` instance does not have a defined file extension.
178
+ """
179
+ if batch_file.extension is None:
180
+ raise ValueError(f"The `BatchFile` must have a defined file extension. "
181
+ f"Received '{batch_file.extension}.")
109
182
  self._batch_files[batch_file.extension] = batch_file
110
183
 
111
184
 
112
- def get_file(self, extension: str) -> BatchFile:
113
- """Get a batch file instance from the batch, according to the file extension."""
185
+ def get_file(
186
+ self,
187
+ extension: str
188
+ ) -> BatchFile:
189
+ """Get a batch file instance from the batch, according to the file extension.
190
+
191
+ :param extension: The file extension of the batch file.
192
+ :raises NotImplementedError: If the extension is undefined for the batch.
193
+ :return: The batch file instance registered under the input file extension.
194
+ """
114
195
  try:
115
196
  return self._batch_files[extension]
116
197
  except KeyError:
117
- raise BatchFileNotFoundError(f"No batch file found with extension '{extension}'")
198
+ raise NotImplementedError(f"A batch file with extension '{extension}' is not implemented for this batch.")
118
199
 
119
200
 
120
- def read_file(self, extension: str):
121
- """Read a file from the batch, according to the file extension."""
122
- batch_file = self.get_file(extension)
123
- return batch_file.read()
124
-
201
+ def delete_file(
202
+ self,
203
+ extension: str
204
+ ) -> None:
205
+ """Delete a file from the batch, according to the file extension.
125
206
 
126
- def delete_file(self, extension: str) -> None:
127
- """Delete a file from the batch, according to the file extension."""
207
+ :param extension: The file extension of the batch file.
208
+ :raises FileNotFoundError: If the batch file does not exist in the file system.
209
+ """
128
210
  batch_file = self.get_file(extension)
129
- try:
130
- batch_file.delete()
131
- except FileNotFoundError as e:
132
- raise BatchFileNotFoundError(str(e))
211
+ batch_file.delete()
133
212
 
134
213
 
135
- def has_file(self, extension: str) -> bool:
136
- """Return true if a file exists in the batch with the input file extension."""
214
+ def has_file(
215
+ self,
216
+ extension: str
217
+ ) -> bool:
218
+ """Determine the existance of a batch file in the file system.
219
+
220
+ :param extension: The file extension of the batch file.
221
+ :return: True if the batch file exists in the file system, False otherwise.
222
+ """
137
223
  try:
138
- # only return true if both
139
- # -> the batch has the extension defined
140
- # -> the file with that extension exists in the batch parent directory
141
224
  batch_file = self.get_file(extension)
142
225
  return batch_file.exists
143
- except BatchFileNotFoundError:
226
+ except FileNotFoundError:
144
227
  return False
145
-
146
-
228
+
229
+
230
+ def read_spectrogram(
231
+ self
232
+ ) -> Spectrogram:
233
+ """Read and return the spectrogram data stored in the batch.
234
+
235
+ :return: The spectrogram stored by the batch `spectrogram_file`.
236
+ """
237
+ return self.spectrogram_file.read()
238
+