shepherd-core 2023.11.1__py3-none-any.whl → 2024.4.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.
- shepherd_core/__init__.py +3 -2
- shepherd_core/data_models/base/calibration.py +3 -1
- shepherd_core/data_models/base/content.py +13 -11
- shepherd_core/data_models/base/shepherd.py +4 -6
- shepherd_core/data_models/content/energy_environment.py +1 -1
- shepherd_core/data_models/content/virtual_harvester.py +1 -1
- shepherd_core/data_models/content/virtual_source.py +31 -53
- shepherd_core/data_models/experiment/experiment.py +13 -18
- shepherd_core/data_models/experiment/observer_features.py +9 -15
- shepherd_core/data_models/experiment/target_config.py +4 -11
- shepherd_core/data_models/task/__init__.py +2 -6
- shepherd_core/data_models/task/emulation.py +7 -14
- shepherd_core/data_models/task/firmware_mod.py +1 -3
- shepherd_core/data_models/task/harvest.py +1 -3
- shepherd_core/data_models/task/observer_tasks.py +1 -1
- shepherd_core/data_models/task/testbed_tasks.py +7 -3
- shepherd_core/data_models/testbed/cape.py +1 -1
- shepherd_core/data_models/testbed/gpio.py +1 -1
- shepherd_core/data_models/testbed/mcu.py +1 -1
- shepherd_core/data_models/testbed/observer.py +7 -23
- shepherd_core/data_models/testbed/target.py +1 -1
- shepherd_core/data_models/testbed/testbed.py +2 -4
- shepherd_core/decoder_waveform/uart.py +9 -26
- shepherd_core/fw_tools/__init__.py +1 -3
- shepherd_core/fw_tools/converter.py +4 -12
- shepherd_core/fw_tools/patcher.py +4 -12
- shepherd_core/fw_tools/validation.py +1 -2
- shepherd_core/inventory/__init__.py +53 -9
- shepherd_core/inventory/system.py +10 -5
- shepherd_core/logger.py +1 -3
- shepherd_core/reader.py +45 -57
- shepherd_core/testbed_client/cache_path.py +15 -0
- shepherd_core/testbed_client/client.py +7 -19
- shepherd_core/testbed_client/fixtures.py +8 -15
- shepherd_core/testbed_client/user_model.py +7 -7
- shepherd_core/vsource/virtual_converter_model.py +5 -15
- shepherd_core/vsource/virtual_harvester_model.py +2 -3
- shepherd_core/vsource/virtual_source_model.py +3 -6
- shepherd_core/writer.py +16 -24
- {shepherd_core-2023.11.1.dist-info → shepherd_core-2024.4.1.dist-info}/METADATA +50 -38
- shepherd_core-2024.4.1.dist-info/RECORD +64 -0
- {shepherd_core-2023.11.1.dist-info → shepherd_core-2024.4.1.dist-info}/WHEEL +1 -1
- {shepherd_core-2023.11.1.dist-info → shepherd_core-2024.4.1.dist-info}/top_level.txt +0 -1
- shepherd_core/data_models/content/_external_fixtures.yaml +0 -394
- shepherd_core/data_models/content/energy_environment_fixture.yaml +0 -50
- shepherd_core/data_models/content/virtual_harvester_fixture.yaml +0 -159
- shepherd_core/data_models/content/virtual_source_fixture.yaml +0 -229
- shepherd_core/data_models/testbed/cape_fixture.yaml +0 -94
- shepherd_core/data_models/testbed/gpio_fixture.yaml +0 -166
- shepherd_core/data_models/testbed/mcu_fixture.yaml +0 -19
- shepherd_core/data_models/testbed/observer_fixture.yaml +0 -220
- shepherd_core/data_models/testbed/target_fixture.yaml +0 -137
- shepherd_core/data_models/testbed/testbed_fixture.yaml +0 -25
- shepherd_core-2023.11.1.dist-info/RECORD +0 -117
- tests/__init__.py +0 -0
- tests/conftest.py +0 -64
- tests/data_models/__init__.py +0 -0
- tests/data_models/conftest.py +0 -14
- tests/data_models/example_cal_data.yaml +0 -31
- tests/data_models/example_cal_data_faulty.yaml +0 -29
- tests/data_models/example_cal_meas.yaml +0 -178
- tests/data_models/example_cal_meas_faulty1.yaml +0 -142
- tests/data_models/example_cal_meas_faulty2.yaml +0 -136
- tests/data_models/example_config_emulator.yaml +0 -41
- tests/data_models/example_config_experiment.yaml +0 -16
- tests/data_models/example_config_experiment_alternative.yaml +0 -14
- tests/data_models/example_config_harvester.yaml +0 -15
- tests/data_models/example_config_testbed.yaml +0 -26
- tests/data_models/example_config_virtsource.yaml +0 -78
- tests/data_models/test_base_models.py +0 -205
- tests/data_models/test_content_fixtures.py +0 -41
- tests/data_models/test_content_models.py +0 -288
- tests/data_models/test_examples.py +0 -48
- tests/data_models/test_experiment_models.py +0 -279
- tests/data_models/test_task_generation.py +0 -52
- tests/data_models/test_task_models.py +0 -131
- tests/data_models/test_testbed_fixtures.py +0 -47
- tests/data_models/test_testbed_models.py +0 -187
- tests/decoder_waveform/__init__.py +0 -0
- tests/decoder_waveform/test_decoder.py +0 -34
- tests/fw_tools/__init__.py +0 -0
- tests/fw_tools/conftest.py +0 -5
- tests/fw_tools/test_converter.py +0 -76
- tests/fw_tools/test_patcher.py +0 -66
- tests/fw_tools/test_validation.py +0 -56
- tests/inventory/__init__.py +0 -0
- tests/inventory/test_inventory.py +0 -22
- tests/test_cal_hw.py +0 -38
- tests/test_examples.py +0 -40
- tests/test_logger.py +0 -15
- tests/test_reader.py +0 -283
- tests/test_writer.py +0 -169
- tests/testbed_client/__init__.py +0 -0
- tests/vsource/__init__.py +0 -0
- tests/vsource/conftest.py +0 -51
- tests/vsource/test_converter.py +0 -165
- tests/vsource/test_harvester.py +0 -79
- tests/vsource/test_z.py +0 -5
- {shepherd_core-2023.11.1.dist-info → shepherd_core-2024.4.1.dist-info}/zip-safe +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
from typing import List
|
|
2
2
|
from typing import Optional
|
|
3
3
|
|
|
4
|
-
from pydantic import EmailStr
|
|
5
4
|
from pydantic import Field
|
|
6
5
|
from pydantic import validate_call
|
|
7
6
|
from typing_extensions import Annotated
|
|
8
7
|
from typing_extensions import Self
|
|
9
8
|
|
|
9
|
+
from ..base.content import IdInt
|
|
10
10
|
from ..base.content import NameStr
|
|
11
11
|
from ..base.shepherd import ShpModel
|
|
12
12
|
from ..experiment.experiment import Experiment
|
|
@@ -21,7 +21,10 @@ class TestbedTasks(ShpModel):
|
|
|
21
21
|
observer_tasks: Annotated[List[ObserverTasks], Field(min_length=1, max_length=128)]
|
|
22
22
|
|
|
23
23
|
# POST PROCESS
|
|
24
|
-
|
|
24
|
+
email_results: bool = False
|
|
25
|
+
owner_id: Optional[IdInt]
|
|
26
|
+
# TODO: had real email previously, does it really need these at all?
|
|
27
|
+
# DB stores experiment and knows when to email
|
|
25
28
|
|
|
26
29
|
@classmethod
|
|
27
30
|
@validate_call
|
|
@@ -34,7 +37,8 @@ class TestbedTasks(ShpModel):
|
|
|
34
37
|
return cls(
|
|
35
38
|
name=xp.name,
|
|
36
39
|
observer_tasks=obs_tasks,
|
|
37
|
-
|
|
40
|
+
email_results=xp.email_results,
|
|
41
|
+
owner_id=xp.owner_id,
|
|
38
42
|
)
|
|
39
43
|
|
|
40
44
|
def get_observer_tasks(self, observer: str) -> Optional[ObserverTasks]:
|
|
@@ -26,7 +26,7 @@ class Direction(str, Enum):
|
|
|
26
26
|
class GPIO(ShpModel, title="GPIO of Observer Node"):
|
|
27
27
|
"""meta-data representation of a testbed-component"""
|
|
28
28
|
|
|
29
|
-
id: IdInt
|
|
29
|
+
id: IdInt
|
|
30
30
|
name: NameStr
|
|
31
31
|
description: Optional[SafeStr] = None
|
|
32
32
|
comment: Optional[SafeStr] = None
|
|
@@ -26,7 +26,7 @@ class ProgrammerProtocol(str, Enum):
|
|
|
26
26
|
class MCU(ShpModel, title="Microcontroller of the Target Node"):
|
|
27
27
|
"""meta-data representation of a testbed-component (physical object)"""
|
|
28
28
|
|
|
29
|
-
id: IdInt
|
|
29
|
+
id: IdInt
|
|
30
30
|
name: NameStr
|
|
31
31
|
description: SafeStr
|
|
32
32
|
comment: Optional[SafeStr] = None
|
|
@@ -19,16 +19,14 @@ from .target import Target
|
|
|
19
19
|
|
|
20
20
|
MACStr = Annotated[
|
|
21
21
|
str,
|
|
22
|
-
StringConstraints(
|
|
23
|
-
max_length=17, pattern=r"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$"
|
|
24
|
-
),
|
|
22
|
+
StringConstraints(max_length=17, pattern=r"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$"),
|
|
25
23
|
]
|
|
26
24
|
|
|
27
25
|
|
|
28
26
|
class Observer(ShpModel, title="Shepherd-Sheep"):
|
|
29
27
|
"""meta-data representation of a testbed-component (physical object)"""
|
|
30
28
|
|
|
31
|
-
id: IdInt
|
|
29
|
+
id: IdInt
|
|
32
30
|
name: NameStr
|
|
33
31
|
description: SafeStr
|
|
34
32
|
comment: Optional[SafeStr] = None
|
|
@@ -65,23 +63,13 @@ class Observer(ShpModel, title="Shepherd-Sheep"):
|
|
|
65
63
|
has_cape = self.cape is not None
|
|
66
64
|
has_target = (self.target_a is not None) or (self.target_b is not None)
|
|
67
65
|
if not has_cape and has_target:
|
|
68
|
-
raise ValueError(
|
|
69
|
-
f"Observer '{self.name}' is faulty " f"-> has targets but no cape"
|
|
70
|
-
)
|
|
66
|
+
raise ValueError(f"Observer '{self.name}' is faulty -> has targets but no cape")
|
|
71
67
|
return self
|
|
72
68
|
|
|
73
69
|
def has_target(self, target_id: int) -> bool:
|
|
74
|
-
if
|
|
75
|
-
self.target_a is not None
|
|
76
|
-
and target_id == self.target_a.id
|
|
77
|
-
and self.target_a.active
|
|
78
|
-
):
|
|
70
|
+
if self.target_a is not None and target_id == self.target_a.id and self.target_a.active:
|
|
79
71
|
return True
|
|
80
|
-
if
|
|
81
|
-
self.target_b is not None
|
|
82
|
-
and target_id == self.target_b.id
|
|
83
|
-
and self.target_b.active
|
|
84
|
-
):
|
|
72
|
+
if self.target_b is not None and target_id == self.target_b.id and self.target_b.active:
|
|
85
73
|
return True
|
|
86
74
|
return False
|
|
87
75
|
|
|
@@ -91,9 +79,7 @@ class Observer(ShpModel, title="Shepherd-Sheep"):
|
|
|
91
79
|
return TargetPort.A
|
|
92
80
|
if self.target_b is not None and target_id == self.target_b.id:
|
|
93
81
|
return TargetPort.B
|
|
94
|
-
raise ValueError(
|
|
95
|
-
f"Target-ID {target_id} was not found in Observer '{self.name}'"
|
|
96
|
-
)
|
|
82
|
+
raise ValueError(f"Target-ID {target_id} was not found in Observer '{self.name}'")
|
|
97
83
|
|
|
98
84
|
def get_target(self, target_id: int) -> Target:
|
|
99
85
|
if self.has_target(target_id):
|
|
@@ -101,6 +87,4 @@ class Observer(ShpModel, title="Shepherd-Sheep"):
|
|
|
101
87
|
return self.target_a
|
|
102
88
|
if self.target_b is not None and target_id == self.target_b.id:
|
|
103
89
|
return self.target_b
|
|
104
|
-
raise ValueError(
|
|
105
|
-
f"Target-ID {target_id} was not found in Observer '{self.name}'"
|
|
106
|
-
)
|
|
90
|
+
raise ValueError(f"Target-ID {target_id} was not found in Observer '{self.name}'")
|
|
@@ -21,7 +21,7 @@ MCUPort = Annotated[int, Field(ge=1, le=2)]
|
|
|
21
21
|
class Target(ShpModel, title="Target Node (DuT)"):
|
|
22
22
|
"""meta-data representation of a testbed-component (physical object)"""
|
|
23
23
|
|
|
24
|
-
id: IdInt
|
|
24
|
+
id: IdInt
|
|
25
25
|
name: NameStr
|
|
26
26
|
version: NameStr
|
|
27
27
|
description: SafeStr
|
|
@@ -20,7 +20,7 @@ from .observer import Observer
|
|
|
20
20
|
class Testbed(ShpModel):
|
|
21
21
|
"""meta-data representation of a testbed-component (physical object)"""
|
|
22
22
|
|
|
23
|
-
id: IdInt
|
|
23
|
+
id: IdInt
|
|
24
24
|
name: NameStr
|
|
25
25
|
description: SafeStr
|
|
26
26
|
comment: Optional[SafeStr] = None
|
|
@@ -87,6 +87,4 @@ class Testbed(ShpModel):
|
|
|
87
87
|
continue
|
|
88
88
|
if _observer.has_target(target_id):
|
|
89
89
|
return _observer
|
|
90
|
-
raise ValueError(
|
|
91
|
-
f"Target-ID {target_id} was not found in Testbed '{self.name}'"
|
|
92
|
-
)
|
|
90
|
+
raise ValueError(f"Target-ID {target_id} was not found in Testbed '{self.name}'")
|
|
@@ -17,6 +17,7 @@ https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter
|
|
|
17
17
|
https://sigrok.org/wiki/Protocol_decoder:Uart
|
|
18
18
|
|
|
19
19
|
"""
|
|
20
|
+
|
|
20
21
|
from enum import Enum
|
|
21
22
|
from pathlib import Path
|
|
22
23
|
from typing import Optional
|
|
@@ -56,9 +57,7 @@ class Uart:
|
|
|
56
57
|
(some detectors still missing)
|
|
57
58
|
"""
|
|
58
59
|
if isinstance(content, Path):
|
|
59
|
-
self.events_sig: np.ndarray = np.loadtxt(
|
|
60
|
-
content.as_posix(), delimiter=",", skiprows=1
|
|
61
|
-
)
|
|
60
|
+
self.events_sig: np.ndarray = np.loadtxt(content.as_posix(), delimiter=",", skiprows=1)
|
|
62
61
|
# TODO: if float fails load as str -
|
|
63
62
|
# cast first col as np.datetime64 with ns-resolution, convert to delta
|
|
64
63
|
else:
|
|
@@ -66,9 +65,7 @@ class Uart:
|
|
|
66
65
|
|
|
67
66
|
# verify table
|
|
68
67
|
if self.events_sig.shape[1] != 2:
|
|
69
|
-
raise TypeError(
|
|
70
|
-
"Input file should have 2 rows -> (comma-separated) timestamp & value"
|
|
71
|
-
)
|
|
68
|
+
raise TypeError("Input file should have 2 rows -> (comma-separated) timestamp & value")
|
|
72
69
|
if self.events_sig.shape[0] < 8:
|
|
73
70
|
raise TypeError("Input file is too short (< state-changes)")
|
|
74
71
|
# verify timestamps
|
|
@@ -80,23 +77,17 @@ class Uart:
|
|
|
80
77
|
self._convert_analog2digital()
|
|
81
78
|
self._filter_redundant_states()
|
|
82
79
|
|
|
83
|
-
self.baud_rate: int = (
|
|
84
|
-
baud_rate if baud_rate is not None else self.detect_baud_rate()
|
|
85
|
-
)
|
|
80
|
+
self.baud_rate: int = baud_rate if baud_rate is not None else self.detect_baud_rate()
|
|
86
81
|
self.dur_tick: float = 1.0 / self.baud_rate
|
|
87
82
|
|
|
88
83
|
self._add_duration()
|
|
89
84
|
|
|
90
|
-
self.inversion: bool = (
|
|
91
|
-
inversion if inversion is not None else self.detect_inversion()
|
|
92
|
-
)
|
|
85
|
+
self.inversion: bool = inversion if inversion is not None else self.detect_inversion()
|
|
93
86
|
self.half_stop: bool = self.detect_half_stop() # not needed ATM
|
|
94
87
|
|
|
95
88
|
# TODO: add detectors
|
|
96
89
|
self.parity: Parity = parity if parity is not None else Parity.no
|
|
97
|
-
self.bit_order: BitOrder =
|
|
98
|
-
bit_order if bit_order is not None else BitOrder.lsb_first
|
|
99
|
-
)
|
|
90
|
+
self.bit_order: BitOrder = bit_order if bit_order is not None else BitOrder.lsb_first
|
|
100
91
|
self.frame_length: int = frame_length if frame_length is not None else 8
|
|
101
92
|
|
|
102
93
|
if not (0 < self.frame_length <= 64):
|
|
@@ -146,14 +137,10 @@ class Uart:
|
|
|
146
137
|
logger.warning("Tried to add state-duration, but it seems already present")
|
|
147
138
|
return
|
|
148
139
|
if not hasattr(self, "dur_tick"):
|
|
149
|
-
raise ValueError(
|
|
150
|
-
"Make sure that baud-rate was calculated before running add_dur()"
|
|
151
|
-
)
|
|
140
|
+
raise ValueError("Make sure that baud-rate was calculated before running add_dur()")
|
|
152
141
|
dur_steps = self.events_sig[1:, 0] - self.events_sig[:-1, 0]
|
|
153
142
|
dur_steps = np.reshape(dur_steps, (dur_steps.size, 1))
|
|
154
|
-
self.events_sig = np.append(
|
|
155
|
-
self.events_sig[:-1, :], dur_steps / self.dur_tick, axis=1
|
|
156
|
-
)
|
|
143
|
+
self.events_sig = np.append(self.events_sig[:-1, :], dur_steps / self.dur_tick, axis=1)
|
|
157
144
|
|
|
158
145
|
def detect_inversion(self) -> bool:
|
|
159
146
|
"""Analyze bit-state during long pauses (unchanged states)
|
|
@@ -182,16 +169,12 @@ class Uart:
|
|
|
182
169
|
def detect_half_stop(self) -> bool:
|
|
183
170
|
"""Looks into the spacing between time-steps"""
|
|
184
171
|
events = self.events_sig[:1000, :] # speedup for large datasets
|
|
185
|
-
return (
|
|
186
|
-
np.sum((events > 1.333 * self.dur_tick) & (events < 1.667 * self.dur_tick))
|
|
187
|
-
> 0
|
|
188
|
-
)
|
|
172
|
+
return np.sum((events > 1.333 * self.dur_tick) & (events < 1.667 * self.dur_tick)) > 0
|
|
189
173
|
|
|
190
174
|
def detect_dataframe_length(self) -> int:
|
|
191
175
|
"""Look after longest pauses
|
|
192
176
|
- accumulate steps until a state with uneven step-size is found
|
|
193
177
|
"""
|
|
194
|
-
pass
|
|
195
178
|
|
|
196
179
|
def get_symbols(self, *, force_redo: bool = False) -> np.ndarray:
|
|
197
180
|
"""Ways to detect EOF:
|
|
@@ -25,9 +25,7 @@ except ImportError:
|
|
|
25
25
|
"cffi",
|
|
26
26
|
]
|
|
27
27
|
# only update when module is not avail
|
|
28
|
-
MOCK_MODULES = [
|
|
29
|
-
mod_name for mod_name in MOCK_MODULES if find_spec(mod_name) is None
|
|
30
|
-
]
|
|
28
|
+
MOCK_MODULES = [mod_name for mod_name in MOCK_MODULES if find_spec(mod_name) is None]
|
|
31
29
|
sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES)
|
|
32
30
|
|
|
33
31
|
from .converter import base64_to_file
|
|
@@ -22,9 +22,7 @@ def firmware_to_hex(file_path: Path) -> Path:
|
|
|
22
22
|
return elf_to_hex(file_path)
|
|
23
23
|
if is_hex(file_path):
|
|
24
24
|
return file_path
|
|
25
|
-
raise ValueError(
|
|
26
|
-
"FW2Hex: unknown file '%s', it should be ELF or HEX", file_path.name
|
|
27
|
-
)
|
|
25
|
+
raise ValueError("FW2Hex: unknown file '%s', it should be ELF or HEX", file_path.name)
|
|
28
26
|
|
|
29
27
|
|
|
30
28
|
@validate_call
|
|
@@ -72,9 +70,7 @@ def base64_to_hash(content: str) -> str:
|
|
|
72
70
|
|
|
73
71
|
|
|
74
72
|
@validate_call
|
|
75
|
-
def extract_firmware(
|
|
76
|
-
data: Union[str, Path], data_type: FirmwareDType, file_path: Path
|
|
77
|
-
) -> Path:
|
|
73
|
+
def extract_firmware(data: Union[str, Path], data_type: FirmwareDType, file_path: Path) -> Path:
|
|
78
74
|
"""- base64-string will be transformed into file
|
|
79
75
|
- if data is a path the file will be copied to the destination
|
|
80
76
|
"""
|
|
@@ -90,14 +86,10 @@ def extract_firmware(
|
|
|
90
86
|
elif data_type == FirmwareDType.path_hex:
|
|
91
87
|
file = file_path.with_suffix(".hex")
|
|
92
88
|
else:
|
|
93
|
-
raise ValueError(
|
|
94
|
-
"FW-Extraction failed due to unknown datatype '%s'", data_type
|
|
95
|
-
)
|
|
89
|
+
raise ValueError("FW-Extraction failed due to unknown datatype '%s'", data_type)
|
|
96
90
|
if not file.parent.exists():
|
|
97
91
|
file.parent.mkdir(parents=True)
|
|
98
92
|
shutil.copy(data, file)
|
|
99
93
|
else:
|
|
100
|
-
raise ValueError(
|
|
101
|
-
"FW-Extraction failed due to unknown data-type '%s'", type(data)
|
|
102
|
-
)
|
|
94
|
+
raise ValueError("FW-Extraction failed due to unknown data-type '%s'", type(data))
|
|
103
95
|
return file
|
|
@@ -46,9 +46,7 @@ def find_symbol(file_elf: Path, symbol: str) -> bool:
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
@validate_call
|
|
49
|
-
def read_symbol(
|
|
50
|
-
file_elf: Path, symbol: str, length: int = uid_len_default
|
|
51
|
-
) -> Optional[int]:
|
|
49
|
+
def read_symbol(file_elf: Path, symbol: str, length: int = uid_len_default) -> Optional[int]:
|
|
52
50
|
"""Interpreted as int"""
|
|
53
51
|
if not find_symbol(file_elf, symbol):
|
|
54
52
|
return None
|
|
@@ -99,9 +97,7 @@ def modify_symbol_value(
|
|
|
99
97
|
value_raw = elf.read(address=addr, count=uid_len_default)[-uid_len_default:]
|
|
100
98
|
# ⤷ cutting needed -> msp produces 4b instead of 2
|
|
101
99
|
value_old = int.from_bytes(bytes=value_raw, byteorder=elf.endian, signed=False)
|
|
102
|
-
value_raw = value.to_bytes(
|
|
103
|
-
length=uid_len_default, byteorder=elf.endian, signed=False
|
|
104
|
-
)
|
|
100
|
+
value_raw = value.to_bytes(length=uid_len_default, byteorder=elf.endian, signed=False)
|
|
105
101
|
try:
|
|
106
102
|
elf.write(address=addr, data=value_raw)
|
|
107
103
|
except AttributeError:
|
|
@@ -110,9 +106,7 @@ def modify_symbol_value(
|
|
|
110
106
|
if overwrite:
|
|
111
107
|
file_new = file_elf
|
|
112
108
|
else:
|
|
113
|
-
file_new = file_elf.with_name(
|
|
114
|
-
file_elf.stem + "_" + str(value) + file_elf.suffix
|
|
115
|
-
)
|
|
109
|
+
file_new = file_elf.with_name(file_elf.stem + "_" + str(value) + file_elf.suffix)
|
|
116
110
|
# could be simplified, but py3.8-- doesn't know .with_stem()
|
|
117
111
|
elf.save(path=file_new)
|
|
118
112
|
elf.close()
|
|
@@ -127,6 +121,4 @@ def modify_symbol_value(
|
|
|
127
121
|
|
|
128
122
|
|
|
129
123
|
def modify_uid(file_elf: Path, value: int) -> Optional[Path]:
|
|
130
|
-
return modify_symbol_value(
|
|
131
|
-
file_elf, symbol=uid_str_default, value=value, overwrite=True
|
|
132
|
-
)
|
|
124
|
+
return modify_symbol_value(file_elf, symbol=uid_str_default, value=value, overwrite=True)
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
- system-parameters
|
|
4
4
|
- hardware-config
|
|
5
5
|
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from datetime import timedelta
|
|
6
9
|
from pathlib import Path
|
|
7
10
|
from typing import List
|
|
8
11
|
|
|
@@ -26,20 +29,20 @@ __all__ = [
|
|
|
26
29
|
|
|
27
30
|
class Inventory(PythonInventory, SystemInventory, TargetInventory):
|
|
28
31
|
# has all child-parameters
|
|
32
|
+
hostname: str
|
|
33
|
+
created: datetime
|
|
29
34
|
|
|
30
35
|
@classmethod
|
|
31
36
|
def collect(cls) -> Self:
|
|
32
37
|
# one by one for more precise error messages
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
exclude_unset=True, exclude_defaults=True
|
|
38
|
-
)
|
|
39
|
-
tid = TargetInventory.collect().model_dump(
|
|
40
|
-
exclude_unset=True, exclude_defaults=True
|
|
41
|
-
)
|
|
38
|
+
# NOTE: system is first, as it must take a precise timestamp
|
|
39
|
+
sid = SystemInventory.collect().model_dump(exclude_unset=True, exclude_defaults=True)
|
|
40
|
+
pid = PythonInventory.collect().model_dump(exclude_unset=True, exclude_defaults=True)
|
|
41
|
+
tid = TargetInventory.collect().model_dump(exclude_unset=True, exclude_defaults=True)
|
|
42
42
|
model = {**pid, **sid, **tid}
|
|
43
|
+
# make important metadata available at root level
|
|
44
|
+
model["created"] = sid["timestamp"]
|
|
45
|
+
model["hostname"] = sid["hostname"]
|
|
43
46
|
return cls(**model)
|
|
44
47
|
|
|
45
48
|
|
|
@@ -59,3 +62,44 @@ class InventoryList(ShpModel):
|
|
|
59
62
|
content = list(item.model_dump().values())
|
|
60
63
|
content = ["" if value is None else str(value) for value in content]
|
|
61
64
|
fd.write(", ".join(content) + "\r\n")
|
|
65
|
+
|
|
66
|
+
def warn(self) -> dict:
|
|
67
|
+
warnings = {}
|
|
68
|
+
ts_earl = min([_e.created.timestamp() for _e in self.elements])
|
|
69
|
+
for _e in self.elements:
|
|
70
|
+
if _e.uptime > timedelta(hours=30).total_seconds():
|
|
71
|
+
warnings["uptime"] = f"[{self.hostname}] restart is recommended"
|
|
72
|
+
if (_e.created.timestamp() - ts_earl) > 10:
|
|
73
|
+
warnings["time_delta"] = f"[{self.hostname}] time-sync has failed"
|
|
74
|
+
|
|
75
|
+
# turn dict[hostname][type] = val
|
|
76
|
+
# to dict[type][val] = list[hostnames]
|
|
77
|
+
_inp = {
|
|
78
|
+
_e.hostname: _e.model_dump(exclude_unset=True, exclude_defaults=True)
|
|
79
|
+
for _e in self.elements
|
|
80
|
+
}
|
|
81
|
+
result = {}
|
|
82
|
+
for _host, _types in _inp.items():
|
|
83
|
+
for _type, _val in _types.items():
|
|
84
|
+
if _type not in result:
|
|
85
|
+
result[_type] = {}
|
|
86
|
+
if _val not in result[_type]:
|
|
87
|
+
result[_type][_val] = []
|
|
88
|
+
result[_type][_val].append(_host)
|
|
89
|
+
rescnt = {_key: len(_val) for _key, _val in result.items()}
|
|
90
|
+
t_unique = [
|
|
91
|
+
"h5py",
|
|
92
|
+
"numpy",
|
|
93
|
+
"pydantic",
|
|
94
|
+
"python",
|
|
95
|
+
"shepherd_core",
|
|
96
|
+
"shepherd_sheep",
|
|
97
|
+
"yaml",
|
|
98
|
+
"zstandard",
|
|
99
|
+
]
|
|
100
|
+
for _key in t_unique:
|
|
101
|
+
if rescnt[_key] > 1:
|
|
102
|
+
warnings[_key] = f"[{_key}] VersionMismatch - {result[_key]}"
|
|
103
|
+
|
|
104
|
+
# TODO: finish with more potential warnings
|
|
105
|
+
return warnings
|
|
@@ -2,10 +2,12 @@ import platform
|
|
|
2
2
|
import subprocess
|
|
3
3
|
import time
|
|
4
4
|
from contextlib import suppress
|
|
5
|
+
from datetime import datetime
|
|
5
6
|
from typing import Optional
|
|
6
7
|
|
|
7
8
|
from typing_extensions import Self
|
|
8
9
|
|
|
10
|
+
from .. import local_now
|
|
9
11
|
from .. import logger
|
|
10
12
|
|
|
11
13
|
try:
|
|
@@ -22,6 +24,9 @@ from ..data_models import ShpModel
|
|
|
22
24
|
class SystemInventory(ShpModel):
|
|
23
25
|
uptime: PositiveInt
|
|
24
26
|
# ⤷ seconds
|
|
27
|
+
timestamp: datetime
|
|
28
|
+
# time_delta: timedelta = timedelta(0) # noqa: ERA001
|
|
29
|
+
# ⤷ lag behind earliest observer, TODO: wrong place
|
|
25
30
|
|
|
26
31
|
system: str
|
|
27
32
|
release: str
|
|
@@ -43,6 +48,8 @@ class SystemInventory(ShpModel):
|
|
|
43
48
|
|
|
44
49
|
@classmethod
|
|
45
50
|
def collect(cls) -> Self:
|
|
51
|
+
ts = local_now()
|
|
52
|
+
|
|
46
53
|
if psutil is None:
|
|
47
54
|
ifs2 = {}
|
|
48
55
|
uptime = 0
|
|
@@ -53,15 +60,12 @@ class SystemInventory(ShpModel):
|
|
|
53
60
|
)
|
|
54
61
|
else:
|
|
55
62
|
ifs1 = psutil.net_if_addrs().items()
|
|
56
|
-
ifs2 = {
|
|
57
|
-
name: (_if[1].address, _if[0].address)
|
|
58
|
-
for name, _if in ifs1
|
|
59
|
-
if len(_if) > 1
|
|
60
|
-
}
|
|
63
|
+
ifs2 = {name: (_if[1].address, _if[0].address) for name, _if in ifs1 if len(_if) > 1}
|
|
61
64
|
uptime = time.time() - psutil.boot_time()
|
|
62
65
|
|
|
63
66
|
model_dict = {
|
|
64
67
|
"uptime": round(uptime),
|
|
68
|
+
"timestamp": ts,
|
|
65
69
|
"system": platform.system(),
|
|
66
70
|
"release": platform.release(),
|
|
67
71
|
"version": platform.version(),
|
|
@@ -69,6 +73,7 @@ class SystemInventory(ShpModel):
|
|
|
69
73
|
"processor": platform.processor(),
|
|
70
74
|
"hostname": platform.node(),
|
|
71
75
|
"interfaces": ifs2,
|
|
76
|
+
# TODO: add free space on /
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
with suppress(FileNotFoundError):
|
shepherd_core/logger.py
CHANGED
|
@@ -15,9 +15,7 @@ def get_verbose_level() -> int:
|
|
|
15
15
|
return verbose_level
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
def set_log_verbose_level(
|
|
19
|
-
log_: Union[logging.Logger, logging.Handler], verbose: int
|
|
20
|
-
) -> None:
|
|
18
|
+
def set_log_verbose_level(log_: Union[logging.Logger, logging.Handler], verbose: int) -> None:
|
|
21
19
|
if verbose == 0:
|
|
22
20
|
log_.setLevel(logging.ERROR)
|
|
23
21
|
logging.basicConfig(level=logging.ERROR)
|