spectre-core 0.0.1__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.
- spectre_core/__init__.py +3 -0
- spectre_core/cfg.py +116 -0
- spectre_core/chunks/__init__.py +206 -0
- spectre_core/chunks/base.py +160 -0
- spectre_core/chunks/chunk_register.py +15 -0
- spectre_core/chunks/factory.py +26 -0
- spectre_core/chunks/library/__init__.py +8 -0
- spectre_core/chunks/library/callisto/__init__.py +0 -0
- spectre_core/chunks/library/callisto/chunk.py +101 -0
- spectre_core/chunks/library/fixed/__init__.py +0 -0
- spectre_core/chunks/library/fixed/chunk.py +185 -0
- spectre_core/chunks/library/sweep/__init__.py +0 -0
- spectre_core/chunks/library/sweep/chunk.py +400 -0
- spectre_core/dynamic_imports.py +22 -0
- spectre_core/exceptions.py +17 -0
- spectre_core/file_handlers/base.py +94 -0
- spectre_core/file_handlers/configs.py +269 -0
- spectre_core/file_handlers/json.py +36 -0
- spectre_core/file_handlers/text.py +21 -0
- spectre_core/logging.py +222 -0
- spectre_core/plotting/__init__.py +5 -0
- spectre_core/plotting/base.py +194 -0
- spectre_core/plotting/factory.py +26 -0
- spectre_core/plotting/format.py +19 -0
- spectre_core/plotting/library/__init__.py +7 -0
- spectre_core/plotting/library/frequency_cuts/panel.py +74 -0
- spectre_core/plotting/library/integral_over_frequency/panel.py +34 -0
- spectre_core/plotting/library/spectrogram/panel.py +92 -0
- spectre_core/plotting/library/time_cuts/panel.py +77 -0
- spectre_core/plotting/panel_register.py +13 -0
- spectre_core/plotting/panel_stack.py +148 -0
- spectre_core/receivers/__init__.py +6 -0
- spectre_core/receivers/base.py +415 -0
- spectre_core/receivers/factory.py +19 -0
- spectre_core/receivers/library/__init__.py +7 -0
- spectre_core/receivers/library/rsp1a/__init__.py +0 -0
- spectre_core/receivers/library/rsp1a/gr/__init__.py +0 -0
- spectre_core/receivers/library/rsp1a/gr/fixed.py +104 -0
- spectre_core/receivers/library/rsp1a/gr/sweep.py +129 -0
- spectre_core/receivers/library/rsp1a/receiver.py +68 -0
- spectre_core/receivers/library/rspduo/__init__.py +0 -0
- spectre_core/receivers/library/rspduo/gr/__init__.py +0 -0
- spectre_core/receivers/library/rspduo/gr/tuner_1_fixed.py +110 -0
- spectre_core/receivers/library/rspduo/gr/tuner_1_sweep.py +135 -0
- spectre_core/receivers/library/rspduo/receiver.py +68 -0
- spectre_core/receivers/library/test/__init__.py +0 -0
- spectre_core/receivers/library/test/gr/__init__.py +0 -0
- spectre_core/receivers/library/test/gr/cosine_signal_1.py +83 -0
- spectre_core/receivers/library/test/gr/tagged_staircase.py +93 -0
- spectre_core/receivers/library/test/receiver.py +174 -0
- spectre_core/receivers/receiver_register.py +22 -0
- spectre_core/receivers/validators.py +205 -0
- spectre_core/spectrograms/__init__.py +3 -0
- spectre_core/spectrograms/analytical.py +205 -0
- spectre_core/spectrograms/array_operations.py +77 -0
- spectre_core/spectrograms/spectrogram.py +461 -0
- spectre_core/spectrograms/transform.py +267 -0
- spectre_core/watchdog/__init__.py +6 -0
- spectre_core/watchdog/base.py +105 -0
- spectre_core/watchdog/event_handler_register.py +15 -0
- spectre_core/watchdog/factory.py +22 -0
- spectre_core/watchdog/library/__init__.py +10 -0
- spectre_core/watchdog/library/fixed/__init__.py +0 -0
- spectre_core/watchdog/library/fixed/event_handler.py +41 -0
- spectre_core/watchdog/library/sweep/event_handler.py +55 -0
- spectre_core/watchdog/watcher.py +50 -0
- spectre_core/web_fetch/callisto.py +101 -0
- spectre_core-0.0.1.dist-info/LICENSE +674 -0
- spectre_core-0.0.1.dist-info/METADATA +40 -0
- spectre_core-0.0.1.dist-info/RECORD +72 -0
- spectre_core-0.0.1.dist-info/WHEEL +5 -0
- spectre_core-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,94 @@
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
2
|
+
# This file is part of SPECTRE
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
|
+
|
5
|
+
import os
|
6
|
+
from abc import ABC, abstractmethod
|
7
|
+
from typing import Any, Optional
|
8
|
+
from warnings import warn
|
9
|
+
|
10
|
+
class BaseFileHandler(ABC):
|
11
|
+
def __init__(self,
|
12
|
+
parent_path: str,
|
13
|
+
base_file_name: str,
|
14
|
+
extension: Optional[str] = None):
|
15
|
+
self._parent_path = parent_path
|
16
|
+
self._base_file_name = base_file_name
|
17
|
+
self._extension = extension
|
18
|
+
|
19
|
+
|
20
|
+
@abstractmethod
|
21
|
+
def read(self) -> Any:
|
22
|
+
pass
|
23
|
+
|
24
|
+
|
25
|
+
@property
|
26
|
+
def parent_path(self) -> str:
|
27
|
+
return self._parent_path
|
28
|
+
|
29
|
+
|
30
|
+
@property
|
31
|
+
def base_file_name(self) -> str:
|
32
|
+
return self._base_file_name
|
33
|
+
|
34
|
+
|
35
|
+
@property
|
36
|
+
def extension(self) -> Optional[str]:
|
37
|
+
return self._extension
|
38
|
+
|
39
|
+
|
40
|
+
@property
|
41
|
+
def file_name(self) -> str:
|
42
|
+
return self._base_file_name if (self._extension is None) else f"{self._base_file_name}.{self._extension}"
|
43
|
+
|
44
|
+
|
45
|
+
@property
|
46
|
+
def file_path(self) -> str:
|
47
|
+
return os.path.join(self._parent_path, self.file_name)
|
48
|
+
|
49
|
+
|
50
|
+
@property
|
51
|
+
def exists(self) -> bool:
|
52
|
+
return os.path.exists(self.file_path)
|
53
|
+
|
54
|
+
|
55
|
+
def make_parent_path(self) -> None:
|
56
|
+
os.makedirs(self.parent_path, exist_ok=True)
|
57
|
+
|
58
|
+
|
59
|
+
def delete(self,
|
60
|
+
doublecheck_delete = True) -> None:
|
61
|
+
if not self.exists:
|
62
|
+
warn(f"{self.file_path} does not exist. No deletion taking place")
|
63
|
+
return
|
64
|
+
else:
|
65
|
+
if doublecheck_delete:
|
66
|
+
self.doublecheck_delete()
|
67
|
+
os.remove(self.file_path)
|
68
|
+
|
69
|
+
|
70
|
+
def cat(self) -> None:
|
71
|
+
print(self.read())
|
72
|
+
|
73
|
+
|
74
|
+
def _doublecheck_action(self,
|
75
|
+
action_message: str) -> None:
|
76
|
+
proceed_with_action = False
|
77
|
+
while not proceed_with_action:
|
78
|
+
user_input = input(f"{action_message} [y/n]: ").strip().lower()
|
79
|
+
if user_input == "y":
|
80
|
+
proceed_with_action = True
|
81
|
+
elif user_input == "n":
|
82
|
+
print("Operation cancelled by the user")
|
83
|
+
raise exit(1)
|
84
|
+
else:
|
85
|
+
print(f"Please enter one of [y/n], received {user_input}")
|
86
|
+
proceed_with_action = False
|
87
|
+
|
88
|
+
|
89
|
+
def doublecheck_overwrite(self) -> None:
|
90
|
+
self._doublecheck_action(action_message=f"The file '{self.file_path}' already exists. Overwrite?")
|
91
|
+
|
92
|
+
|
93
|
+
def doublecheck_delete(self) -> None:
|
94
|
+
self._doublecheck_action(action_message=f"Are you sure you would like to delete '{self.file_path}'?")
|
@@ -0,0 +1,269 @@
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
2
|
+
# This file is part of SPECTRE
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
|
+
|
5
|
+
from typing import Any, Optional, Type, Tuple
|
6
|
+
from abc import ABC
|
7
|
+
import ast
|
8
|
+
|
9
|
+
from spectre_core.file_handlers.json import JsonHandler
|
10
|
+
from spectre_core.cfg import JSON_CONFIGS_DIR_PATH
|
11
|
+
from spectre_core.exceptions import InvalidTagError
|
12
|
+
|
13
|
+
|
14
|
+
def _unpack_param(param: str) -> list[str, str]:
|
15
|
+
"""Seperate a string of the form "a=b" into a list [a,b]."""
|
16
|
+
if not param or '=' not in param:
|
17
|
+
raise ValueError(f'Invalid format: "{param}". Expected "KEY=VALUE".')
|
18
|
+
if param.startswith('=') or param.endswith('='):
|
19
|
+
raise ValueError(f'Invalid format: "{param}". Expected "KEY=VALUE".')
|
20
|
+
# remove leading and trailing whitespace.
|
21
|
+
param = param.strip()
|
22
|
+
return param.split('=', 1)
|
23
|
+
|
24
|
+
|
25
|
+
def _params_to_string_dict(params: list[str]) -> dict[str, str]:
|
26
|
+
"""Converts a list with string elements of the form "a=b" into a dictionary where the key value pairs are "a": "b"."""
|
27
|
+
d = {}
|
28
|
+
for param in params:
|
29
|
+
key, value = _unpack_param(param)
|
30
|
+
d[key] = value
|
31
|
+
return d
|
32
|
+
|
33
|
+
|
34
|
+
def _convert_to_dict(v: str) -> dict:
|
35
|
+
"""Evaluate literally a string containing a Python dictionary expression."""
|
36
|
+
return ast.literal_eval(v)
|
37
|
+
|
38
|
+
|
39
|
+
def _convert_to_bool(v: str) -> bool:
|
40
|
+
"""Evaluate literally a string representation of a boolean as a boolean"""
|
41
|
+
v = v.lower()
|
42
|
+
if v in ('true', '1', 't', 'y', 'yes'):
|
43
|
+
return True
|
44
|
+
if v in ('false', '0', 'f', 'n', 'no'):
|
45
|
+
return False
|
46
|
+
raise ValueError(f'Cannot convert {v} to bool.')
|
47
|
+
|
48
|
+
|
49
|
+
def _convert_string_to_type(value: str,
|
50
|
+
target_type: Type) -> Any:
|
51
|
+
"""Cast a string as the target type."""
|
52
|
+
if target_type == bool:
|
53
|
+
return _convert_to_bool(value)
|
54
|
+
elif target_type == dict:
|
55
|
+
return _convert_to_dict(value)
|
56
|
+
return target_type(value)
|
57
|
+
|
58
|
+
|
59
|
+
def _type_cast_string_dict(d: dict[str, str],
|
60
|
+
type_template: dict[str, Type]) -> dict[str, Any]:
|
61
|
+
"""Cast the values of the input dictionary according to a type template."""
|
62
|
+
casted_d = {}
|
63
|
+
for key, value in d.items():
|
64
|
+
target_type = type_template.get(key)
|
65
|
+
if target_type is None:
|
66
|
+
raise KeyError(f'Key "{key}" not found in type template. Expected keys: {list(type_template.keys())}')
|
67
|
+
try:
|
68
|
+
casted_d[key] = _convert_string_to_type(value, target_type)
|
69
|
+
except ValueError:
|
70
|
+
raise ValueError(f'Failed to convert key "{key}" with value "{value}" to {target_type.__name__}.')
|
71
|
+
return casted_d
|
72
|
+
|
73
|
+
|
74
|
+
def _validate_keys(d: dict[str, Any],
|
75
|
+
type_template: dict[str, type],
|
76
|
+
ignore_keys: Optional[list] = None) -> None:
|
77
|
+
"""Validate that the keys in the input dictionary map one-to-one to the input type template."""
|
78
|
+
if ignore_keys is None:
|
79
|
+
ignore_keys = []
|
80
|
+
|
81
|
+
type_template_keys = set(type_template.keys())
|
82
|
+
input_keys = set(d.keys())
|
83
|
+
ignore_keys = set(ignore_keys)
|
84
|
+
|
85
|
+
missing_keys = type_template_keys - input_keys
|
86
|
+
invalid_keys = input_keys - type_template_keys - ignore_keys
|
87
|
+
|
88
|
+
errors = []
|
89
|
+
|
90
|
+
if missing_keys:
|
91
|
+
errors.append(f"Missing keys: {', '.join(missing_keys)}")
|
92
|
+
|
93
|
+
if invalid_keys:
|
94
|
+
errors.append(f"Invalid keys: {', '.join(invalid_keys)}")
|
95
|
+
|
96
|
+
if errors:
|
97
|
+
raise KeyError("Key errors found! " + " ".join(errors))
|
98
|
+
|
99
|
+
|
100
|
+
def _validate_types(d: dict[str, Any],
|
101
|
+
type_template: dict[str, type],
|
102
|
+
ignore_keys: Optional[list] = None) -> None:
|
103
|
+
"""Validate the types in the input dictionary are consistent with the input type template."""
|
104
|
+
|
105
|
+
if ignore_keys is None:
|
106
|
+
ignore_keys = []
|
107
|
+
|
108
|
+
for k, v in d.items():
|
109
|
+
if k in ignore_keys:
|
110
|
+
continue
|
111
|
+
expected_type = type_template[k]
|
112
|
+
if expected_type is None:
|
113
|
+
raise KeyError(f'Type not found for key "{k}" in the type template.')
|
114
|
+
|
115
|
+
if not isinstance(v, expected_type):
|
116
|
+
raise TypeError(f'Expected {expected_type} for "{k}", but got {type(v)}.')
|
117
|
+
|
118
|
+
|
119
|
+
def validate_against_type_template(d: dict[str, Any],
|
120
|
+
type_template: dict[str, type],
|
121
|
+
ignore_keys: Optional[list] = None) -> None:
|
122
|
+
"""Validates the keys and values of the input dictionary, according to the input type template."""
|
123
|
+
_validate_keys(d,
|
124
|
+
type_template,
|
125
|
+
ignore_keys = ignore_keys)
|
126
|
+
_validate_types(d,
|
127
|
+
type_template,
|
128
|
+
ignore_keys = ignore_keys)
|
129
|
+
|
130
|
+
|
131
|
+
def type_cast_params(params: list[str],
|
132
|
+
type_template: dict[str, type]) -> dict[str, Any]:
|
133
|
+
d = _params_to_string_dict(params)
|
134
|
+
return _type_cast_string_dict(d,
|
135
|
+
type_template)
|
136
|
+
|
137
|
+
|
138
|
+
|
139
|
+
class SPECTREConfig(JsonHandler, ABC):
|
140
|
+
def __init__(self,
|
141
|
+
tag: str,
|
142
|
+
config_type: str,
|
143
|
+
**kwargs):
|
144
|
+
self._validate_tag(tag)
|
145
|
+
self._tag = tag
|
146
|
+
self._config_type = config_type
|
147
|
+
|
148
|
+
self._dict = None # cache
|
149
|
+
super().__init__(JSON_CONFIGS_DIR_PATH,
|
150
|
+
f"{config_type}_config_{tag}",
|
151
|
+
**kwargs)
|
152
|
+
|
153
|
+
|
154
|
+
@property
|
155
|
+
def tag(self) -> str:
|
156
|
+
return self._tag
|
157
|
+
|
158
|
+
|
159
|
+
@property
|
160
|
+
def config_type(self) -> str:
|
161
|
+
return self._config_type
|
162
|
+
|
163
|
+
|
164
|
+
@property
|
165
|
+
def dict(self) -> dict[str, Any]:
|
166
|
+
if self._dict is None:
|
167
|
+
self._dict = self.read()
|
168
|
+
return self._dict
|
169
|
+
|
170
|
+
|
171
|
+
def _validate_tag(self, tag: str) -> None:
|
172
|
+
if "_" in tag:
|
173
|
+
raise InvalidTagError(f"Tags cannot contain an underscore. Received {tag}")
|
174
|
+
if "callisto" in tag:
|
175
|
+
raise InvalidTagError(f'"callisto" cannot be a substring in a native tag. Received "{tag}"')
|
176
|
+
|
177
|
+
|
178
|
+
def __getitem__(self,
|
179
|
+
key: str) -> Any:
|
180
|
+
return self.dict[key]
|
181
|
+
|
182
|
+
|
183
|
+
def get(self,
|
184
|
+
*args,
|
185
|
+
**kwargs) -> Any:
|
186
|
+
return self.dict.get(*args,
|
187
|
+
**kwargs)
|
188
|
+
|
189
|
+
|
190
|
+
def update(self,
|
191
|
+
*args,
|
192
|
+
**kwargs) -> None:
|
193
|
+
self.dict.update(*args, **kwargs)
|
194
|
+
|
195
|
+
|
196
|
+
def items(self):
|
197
|
+
return self.dict.items()
|
198
|
+
|
199
|
+
|
200
|
+
def keys(self):
|
201
|
+
return self.dict.keys()
|
202
|
+
|
203
|
+
|
204
|
+
def values(self):
|
205
|
+
return self.dict.values()
|
206
|
+
|
207
|
+
|
208
|
+
class FitsConfig(SPECTREConfig):
|
209
|
+
|
210
|
+
type_template = {
|
211
|
+
"ORIGIN": str,
|
212
|
+
"TELESCOP": str,
|
213
|
+
"INSTRUME": str,
|
214
|
+
"OBJECT": str,
|
215
|
+
"OBS_LAT": float,
|
216
|
+
"OBS_LONG": float,
|
217
|
+
"OBS_ALT": float
|
218
|
+
}
|
219
|
+
|
220
|
+
def __init__(self,
|
221
|
+
tag: str,
|
222
|
+
**kwargs):
|
223
|
+
super().__init__(tag,
|
224
|
+
"fits",
|
225
|
+
**kwargs)
|
226
|
+
|
227
|
+
def get_create_fits_config_cmd(self,
|
228
|
+
tag: str,
|
229
|
+
as_string: bool = False) -> list[str] | str:
|
230
|
+
command_as_list = ["spectre", "create", "fits-config", "-t", tag]
|
231
|
+
for key, value in self.type_template.items():
|
232
|
+
command_as_list += ["-p"]
|
233
|
+
command_as_list += [f"{key}={value.__name__}"]
|
234
|
+
if as_string:
|
235
|
+
return " ".join(command_as_list)
|
236
|
+
else:
|
237
|
+
return command_as_list
|
238
|
+
|
239
|
+
|
240
|
+
def save_params(self,
|
241
|
+
params: list[str],
|
242
|
+
doublecheck_overwrite: bool = True
|
243
|
+
) -> None:
|
244
|
+
d = type_cast_params(params,
|
245
|
+
self.type_template)
|
246
|
+
self.save(d,
|
247
|
+
doublecheck_overwrite = doublecheck_overwrite)
|
248
|
+
|
249
|
+
|
250
|
+
class CaptureConfig(SPECTREConfig):
|
251
|
+
def __init__(self,
|
252
|
+
tag: str,
|
253
|
+
**kwargs):
|
254
|
+
super().__init__(tag,
|
255
|
+
"capture",
|
256
|
+
**kwargs)
|
257
|
+
|
258
|
+
|
259
|
+
def get_receiver_metadata(self) -> Tuple[str, str]:
|
260
|
+
|
261
|
+
receiver_name, mode = self.get("receiver"), self.get("mode")
|
262
|
+
|
263
|
+
if receiver_name is None:
|
264
|
+
raise ValueError("Invalid capture config! Receiver name is not specified.")
|
265
|
+
|
266
|
+
if mode is None:
|
267
|
+
raise ValueError("Invalid capture config! Receiver mode is not specified.")
|
268
|
+
|
269
|
+
return receiver_name, mode
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
2
|
+
# This file is part of SPECTRE
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
|
+
|
5
|
+
from typing import Any
|
6
|
+
import json
|
7
|
+
|
8
|
+
from spectre_core.file_handlers.base import BaseFileHandler
|
9
|
+
|
10
|
+
class JsonHandler(BaseFileHandler):
|
11
|
+
def __init__(self,
|
12
|
+
parent_path: str,
|
13
|
+
base_file_name: str,
|
14
|
+
extension: str = "json",
|
15
|
+
**kwargs):
|
16
|
+
super().__init__(parent_path,
|
17
|
+
base_file_name,
|
18
|
+
extension,
|
19
|
+
**kwargs)
|
20
|
+
|
21
|
+
|
22
|
+
def read(self) -> dict[str, Any]:
|
23
|
+
with open(self.file_path, 'r') as f:
|
24
|
+
return json.load(f)
|
25
|
+
|
26
|
+
|
27
|
+
def save(self,
|
28
|
+
d: dict,
|
29
|
+
doublecheck_overwrite: bool = True) -> None:
|
30
|
+
self.make_parent_path()
|
31
|
+
|
32
|
+
if self.exists and doublecheck_overwrite:
|
33
|
+
self.doublecheck_overwrite()
|
34
|
+
|
35
|
+
with open(self.file_path, 'w') as file:
|
36
|
+
json.dump(d, file, indent=4)
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
2
|
+
# This file is part of SPECTRE
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
|
+
|
5
|
+
from spectre_core.file_handlers.base import BaseFileHandler
|
6
|
+
|
7
|
+
class TextHandler(BaseFileHandler):
|
8
|
+
def __init__(self,
|
9
|
+
parent_path: str,
|
10
|
+
base_file_name: str,
|
11
|
+
extension: str = "txt",
|
12
|
+
**kwargs):
|
13
|
+
super().__init__(parent_path,
|
14
|
+
base_file_name,
|
15
|
+
extension,
|
16
|
+
**kwargs)
|
17
|
+
|
18
|
+
|
19
|
+
def read(self) -> dict:
|
20
|
+
with open(self.file_path, 'r') as f:
|
21
|
+
return f.read()
|
spectre_core/logging.py
ADDED
@@ -0,0 +1,222 @@
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
2
|
+
# This file is part of SPECTRE
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
|
+
|
5
|
+
|
6
|
+
from logging import getLogger
|
7
|
+
_LOGGER = getLogger(__name__)
|
8
|
+
|
9
|
+
import os
|
10
|
+
import logging
|
11
|
+
from typing import Callable, Optional
|
12
|
+
import warnings
|
13
|
+
from collections import OrderedDict
|
14
|
+
from datetime import datetime
|
15
|
+
|
16
|
+
from spectre_core.file_handlers.text import TextHandler
|
17
|
+
from spectre_core.cfg import (
|
18
|
+
LOGS_DIR_PATH,
|
19
|
+
DEFAULT_DATETIME_FORMAT,
|
20
|
+
get_logs_dir_path
|
21
|
+
)
|
22
|
+
|
23
|
+
PROCESS_TYPES = [
|
24
|
+
"USER",
|
25
|
+
"WORKER"
|
26
|
+
]
|
27
|
+
|
28
|
+
|
29
|
+
def validate_process_type(process_type: str
|
30
|
+
) -> None:
|
31
|
+
if process_type not in PROCESS_TYPES:
|
32
|
+
raise ValueError(f"Invalid process type: {process_type}. Expected one of {PROCESS_TYPES}")
|
33
|
+
|
34
|
+
|
35
|
+
class LogHandler(TextHandler):
|
36
|
+
def __init__(self,
|
37
|
+
datetime_stamp: str,
|
38
|
+
pid: str,
|
39
|
+
process_type: str):
|
40
|
+
self._datetime_stamp = datetime_stamp
|
41
|
+
self._pid = pid
|
42
|
+
validate_process_type(process_type)
|
43
|
+
self._process_type = process_type
|
44
|
+
|
45
|
+
dt = datetime.strptime(datetime_stamp, DEFAULT_DATETIME_FORMAT)
|
46
|
+
date_dir = os.path.join(dt.strftime("%Y"), dt.strftime("%m"), dt.strftime("%d"))
|
47
|
+
parent_path = os.path.join(LOGS_DIR_PATH, date_dir)
|
48
|
+
base_file_name = f"{datetime_stamp}_{pid}_{process_type}"
|
49
|
+
|
50
|
+
super().__init__(parent_path, base_file_name, extension = "log")
|
51
|
+
|
52
|
+
|
53
|
+
@property
|
54
|
+
def datetime_stamp(self) -> str:
|
55
|
+
return self._datetime_stamp
|
56
|
+
|
57
|
+
|
58
|
+
@property
|
59
|
+
def pid(self) -> str:
|
60
|
+
return self._pid
|
61
|
+
|
62
|
+
|
63
|
+
@property
|
64
|
+
def process_type(self) -> str:
|
65
|
+
return self._process_type
|
66
|
+
|
67
|
+
|
68
|
+
class LogHandlers:
|
69
|
+
def __init__(self,
|
70
|
+
process_type: Optional[str] = None,
|
71
|
+
year: Optional[int] = None,
|
72
|
+
month: Optional[int] = None,
|
73
|
+
day: Optional[int] = None):
|
74
|
+
self._log_handler_map: dict[str, LogHandler] = OrderedDict()
|
75
|
+
self._process_type = process_type
|
76
|
+
self.set_date(year, month, day)
|
77
|
+
|
78
|
+
|
79
|
+
@property
|
80
|
+
def process_type(self) -> str:
|
81
|
+
return self._process_type
|
82
|
+
|
83
|
+
|
84
|
+
@property
|
85
|
+
def year(self) -> Optional[int]:
|
86
|
+
return self._year
|
87
|
+
|
88
|
+
|
89
|
+
@property
|
90
|
+
def month(self) -> Optional[int]:
|
91
|
+
return self._month
|
92
|
+
|
93
|
+
|
94
|
+
@property
|
95
|
+
def day(self) -> Optional[int]:
|
96
|
+
return self._day
|
97
|
+
|
98
|
+
|
99
|
+
@property
|
100
|
+
def logs_dir_path(self) -> str:
|
101
|
+
return get_logs_dir_path(self.year, self.month, self.day)
|
102
|
+
|
103
|
+
|
104
|
+
@property
|
105
|
+
def log_handler_map(self) -> dict[str, LogHandler]:
|
106
|
+
if not self._log_handler_map: # check for empty dictionary
|
107
|
+
self._update_log_handler_map()
|
108
|
+
return self._log_handler_map
|
109
|
+
|
110
|
+
|
111
|
+
@property
|
112
|
+
def log_handler_list(self) -> list[LogHandler]:
|
113
|
+
return list(self.log_handler_map.values())
|
114
|
+
|
115
|
+
|
116
|
+
@property
|
117
|
+
def num_logs(self) -> int:
|
118
|
+
return len(self.log_handler_list)
|
119
|
+
|
120
|
+
|
121
|
+
@property
|
122
|
+
def file_names(self) -> list[str]:
|
123
|
+
return list(self.log_handler_map.keys())
|
124
|
+
|
125
|
+
|
126
|
+
def set_date(self,
|
127
|
+
year: Optional[int],
|
128
|
+
month: Optional[int],
|
129
|
+
day: Optional[int]) -> None:
|
130
|
+
self._year = year
|
131
|
+
self._month = month
|
132
|
+
self._day = day
|
133
|
+
self._update_log_handler_map()
|
134
|
+
|
135
|
+
|
136
|
+
def _update_log_handler_map(self) -> None:
|
137
|
+
log_files = [f for (_, _, files) in os.walk(self.logs_dir_path) for f in files]
|
138
|
+
|
139
|
+
if not log_files:
|
140
|
+
warning_message = "No logs found, setting log map to an empty dictionary"
|
141
|
+
_LOGGER.warning(warning_message)
|
142
|
+
warnings.warn(warning_message)
|
143
|
+
return
|
144
|
+
|
145
|
+
for log_file in log_files:
|
146
|
+
file_name, _ = os.path.splitext(log_file)
|
147
|
+
log_start_time, pid, process_type = file_name.split("_")
|
148
|
+
|
149
|
+
if self.process_type and process_type != self.process_type:
|
150
|
+
continue
|
151
|
+
|
152
|
+
self._log_handler_map[file_name] = LogHandler(log_start_time, pid, process_type)
|
153
|
+
|
154
|
+
self._log_handler_map = OrderedDict(sorted(self._log_handler_map.items()))
|
155
|
+
|
156
|
+
|
157
|
+
def update(self) -> None:
|
158
|
+
"""Public alias for setting log handler map"""
|
159
|
+
self._update_log_handler_map()
|
160
|
+
|
161
|
+
|
162
|
+
def __iter__(self):
|
163
|
+
yield from self.log_handler_list
|
164
|
+
|
165
|
+
|
166
|
+
def get_log_handler_from_file_name(self,
|
167
|
+
file_name: str) -> LogHandler:
|
168
|
+
# auto strip the extension if present
|
169
|
+
file_name, _ = os.path.splitext(file_name)
|
170
|
+
try:
|
171
|
+
return self.log_handler_map[file_name]
|
172
|
+
except KeyError:
|
173
|
+
raise FileNotFoundError(f"Log handler for file name '{file_name}' not found in log map")
|
174
|
+
|
175
|
+
|
176
|
+
def get_log_handler_from_pid(self,
|
177
|
+
pid: str) -> LogHandler:
|
178
|
+
for log_handler in self.log_handler_list:
|
179
|
+
if log_handler.pid == pid:
|
180
|
+
return log_handler
|
181
|
+
raise FileNotFoundError(f"Log handler for PID '{pid}' not found in log map")
|
182
|
+
|
183
|
+
|
184
|
+
def configure_root_logger(process_type: str,
|
185
|
+
level: int = logging.INFO
|
186
|
+
) -> LogHandler:
|
187
|
+
system_datetime = datetime.now()
|
188
|
+
datetime_stamp = system_datetime.strftime(DEFAULT_DATETIME_FORMAT)
|
189
|
+
pid = os.getpid()
|
190
|
+
log_handler = LogHandler(datetime_stamp, pid, process_type)
|
191
|
+
log_handler.make_parent_path()
|
192
|
+
|
193
|
+
# configure the root logger
|
194
|
+
logger = logging.getLogger()
|
195
|
+
logger.setLevel(level)
|
196
|
+
# Remove any existing handlers to avoid duplicate logs
|
197
|
+
for handler in logger.handlers:
|
198
|
+
logger.removeHandler(handler)
|
199
|
+
# Set up file handler with specific filename
|
200
|
+
file_handler = logging.FileHandler(log_handler.file_path)
|
201
|
+
file_handler.setLevel(level)
|
202
|
+
formatter = logging.Formatter("[%(asctime)s] [%(levelname)8s] --- %(message)s (%(name)s:%(lineno)s)")
|
203
|
+
file_handler.setFormatter(formatter)
|
204
|
+
# and add it to the root logger
|
205
|
+
logger.addHandler(file_handler)
|
206
|
+
|
207
|
+
return log_handler
|
208
|
+
|
209
|
+
# Logger must be passed in to preserve context of the service function
|
210
|
+
def log_call(logger: logging.Logger
|
211
|
+
) -> Callable:
|
212
|
+
def decorator(func: Callable) -> Callable:
|
213
|
+
def wrapper(*args, **kwargs):
|
214
|
+
try:
|
215
|
+
logger.info(f"Calling the function: {func.__name__}")
|
216
|
+
return func(*args, **kwargs)
|
217
|
+
except Exception as e:
|
218
|
+
logger.error(f"An error occurred while calling the function: {func.__name__}",
|
219
|
+
exc_info=True)
|
220
|
+
raise
|
221
|
+
return wrapper
|
222
|
+
return decorator
|