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.
Files changed (99) hide show
  1. shepherd_core/__init__.py +3 -2
  2. shepherd_core/data_models/base/calibration.py +3 -1
  3. shepherd_core/data_models/base/content.py +13 -11
  4. shepherd_core/data_models/base/shepherd.py +4 -6
  5. shepherd_core/data_models/content/energy_environment.py +1 -1
  6. shepherd_core/data_models/content/virtual_harvester.py +1 -1
  7. shepherd_core/data_models/content/virtual_source.py +31 -53
  8. shepherd_core/data_models/experiment/experiment.py +13 -18
  9. shepherd_core/data_models/experiment/observer_features.py +9 -15
  10. shepherd_core/data_models/experiment/target_config.py +4 -11
  11. shepherd_core/data_models/task/__init__.py +2 -6
  12. shepherd_core/data_models/task/emulation.py +7 -14
  13. shepherd_core/data_models/task/firmware_mod.py +1 -3
  14. shepherd_core/data_models/task/harvest.py +1 -3
  15. shepherd_core/data_models/task/observer_tasks.py +1 -1
  16. shepherd_core/data_models/task/testbed_tasks.py +7 -3
  17. shepherd_core/data_models/testbed/cape.py +1 -1
  18. shepherd_core/data_models/testbed/gpio.py +1 -1
  19. shepherd_core/data_models/testbed/mcu.py +1 -1
  20. shepherd_core/data_models/testbed/observer.py +7 -23
  21. shepherd_core/data_models/testbed/target.py +1 -1
  22. shepherd_core/data_models/testbed/testbed.py +2 -4
  23. shepherd_core/decoder_waveform/uart.py +9 -26
  24. shepherd_core/fw_tools/__init__.py +1 -3
  25. shepherd_core/fw_tools/converter.py +4 -12
  26. shepherd_core/fw_tools/patcher.py +4 -12
  27. shepherd_core/fw_tools/validation.py +1 -2
  28. shepherd_core/inventory/__init__.py +53 -9
  29. shepherd_core/inventory/system.py +10 -5
  30. shepherd_core/logger.py +1 -3
  31. shepherd_core/reader.py +45 -57
  32. shepherd_core/testbed_client/cache_path.py +15 -0
  33. shepherd_core/testbed_client/client.py +7 -19
  34. shepherd_core/testbed_client/fixtures.py +8 -15
  35. shepherd_core/testbed_client/user_model.py +7 -7
  36. shepherd_core/vsource/virtual_converter_model.py +5 -15
  37. shepherd_core/vsource/virtual_harvester_model.py +2 -3
  38. shepherd_core/vsource/virtual_source_model.py +3 -6
  39. shepherd_core/writer.py +16 -24
  40. {shepherd_core-2023.11.1.dist-info → shepherd_core-2024.4.1.dist-info}/METADATA +50 -38
  41. shepherd_core-2024.4.1.dist-info/RECORD +64 -0
  42. {shepherd_core-2023.11.1.dist-info → shepherd_core-2024.4.1.dist-info}/WHEEL +1 -1
  43. {shepherd_core-2023.11.1.dist-info → shepherd_core-2024.4.1.dist-info}/top_level.txt +0 -1
  44. shepherd_core/data_models/content/_external_fixtures.yaml +0 -394
  45. shepherd_core/data_models/content/energy_environment_fixture.yaml +0 -50
  46. shepherd_core/data_models/content/virtual_harvester_fixture.yaml +0 -159
  47. shepherd_core/data_models/content/virtual_source_fixture.yaml +0 -229
  48. shepherd_core/data_models/testbed/cape_fixture.yaml +0 -94
  49. shepherd_core/data_models/testbed/gpio_fixture.yaml +0 -166
  50. shepherd_core/data_models/testbed/mcu_fixture.yaml +0 -19
  51. shepherd_core/data_models/testbed/observer_fixture.yaml +0 -220
  52. shepherd_core/data_models/testbed/target_fixture.yaml +0 -137
  53. shepherd_core/data_models/testbed/testbed_fixture.yaml +0 -25
  54. shepherd_core-2023.11.1.dist-info/RECORD +0 -117
  55. tests/__init__.py +0 -0
  56. tests/conftest.py +0 -64
  57. tests/data_models/__init__.py +0 -0
  58. tests/data_models/conftest.py +0 -14
  59. tests/data_models/example_cal_data.yaml +0 -31
  60. tests/data_models/example_cal_data_faulty.yaml +0 -29
  61. tests/data_models/example_cal_meas.yaml +0 -178
  62. tests/data_models/example_cal_meas_faulty1.yaml +0 -142
  63. tests/data_models/example_cal_meas_faulty2.yaml +0 -136
  64. tests/data_models/example_config_emulator.yaml +0 -41
  65. tests/data_models/example_config_experiment.yaml +0 -16
  66. tests/data_models/example_config_experiment_alternative.yaml +0 -14
  67. tests/data_models/example_config_harvester.yaml +0 -15
  68. tests/data_models/example_config_testbed.yaml +0 -26
  69. tests/data_models/example_config_virtsource.yaml +0 -78
  70. tests/data_models/test_base_models.py +0 -205
  71. tests/data_models/test_content_fixtures.py +0 -41
  72. tests/data_models/test_content_models.py +0 -288
  73. tests/data_models/test_examples.py +0 -48
  74. tests/data_models/test_experiment_models.py +0 -279
  75. tests/data_models/test_task_generation.py +0 -52
  76. tests/data_models/test_task_models.py +0 -131
  77. tests/data_models/test_testbed_fixtures.py +0 -47
  78. tests/data_models/test_testbed_models.py +0 -187
  79. tests/decoder_waveform/__init__.py +0 -0
  80. tests/decoder_waveform/test_decoder.py +0 -34
  81. tests/fw_tools/__init__.py +0 -0
  82. tests/fw_tools/conftest.py +0 -5
  83. tests/fw_tools/test_converter.py +0 -76
  84. tests/fw_tools/test_patcher.py +0 -66
  85. tests/fw_tools/test_validation.py +0 -56
  86. tests/inventory/__init__.py +0 -0
  87. tests/inventory/test_inventory.py +0 -22
  88. tests/test_cal_hw.py +0 -38
  89. tests/test_examples.py +0 -40
  90. tests/test_logger.py +0 -15
  91. tests/test_reader.py +0 -283
  92. tests/test_writer.py +0 -169
  93. tests/testbed_client/__init__.py +0 -0
  94. tests/vsource/__init__.py +0 -0
  95. tests/vsource/conftest.py +0 -51
  96. tests/vsource/test_converter.py +0 -165
  97. tests/vsource/test_harvester.py +0 -79
  98. tests/vsource/test_z.py +0 -5
  99. {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
- email: Optional[EmailStr] = None
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
- email=xp.email_results,
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]:
@@ -24,7 +24,7 @@ class TargetPort(str, Enum):
24
24
  class Cape(ShpModel, title="Shepherd-Cape"):
25
25
  """meta-data representation of a testbed-component (physical object)"""
26
26
 
27
- id: IdInt # noqa: A003
27
+ id: IdInt
28
28
  name: NameStr
29
29
  version: NameStr
30
30
  description: SafeStr
@@ -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 # noqa: A003
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 # noqa: A003
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 # noqa: A003
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 # noqa: A003
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 # noqa: A003
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)
@@ -1,6 +1,5 @@
1
- """TODO: Work in Progress
1
+ """TODO: Work in Progress"""
2
2
 
3
- """
4
3
  import tempfile
5
4
  from pathlib import Path
6
5
 
@@ -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
- pid = PythonInventory.collect().model_dump(
34
- exclude_unset=True, exclude_defaults=True
35
- )
36
- sid = SystemInventory.collect().model_dump(
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)