shepherd-core 2023.12.1__py3-none-any.whl → 2024.4.2__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 (116) hide show
  1. shepherd_core/__init__.py +5 -4
  2. shepherd_core/calibration_hw_def.py +9 -1
  3. shepherd_core/commons.py +2 -0
  4. shepherd_core/data_models/__init__.py +11 -0
  5. shepherd_core/data_models/base/__init__.py +4 -1
  6. shepherd_core/data_models/base/cal_measurement.py +18 -6
  7. shepherd_core/data_models/base/calibration.py +41 -16
  8. shepherd_core/data_models/base/content.py +20 -5
  9. shepherd_core/data_models/base/shepherd.py +23 -12
  10. shepherd_core/data_models/base/timezone.py +5 -0
  11. shepherd_core/data_models/base/wrapper.py +3 -3
  12. shepherd_core/data_models/content/__init__.py +5 -4
  13. shepherd_core/data_models/content/_external_fixtures.yaml +32 -16
  14. shepherd_core/data_models/content/energy_environment.py +7 -5
  15. shepherd_core/data_models/content/energy_environment_fixture.yaml +3 -0
  16. shepherd_core/data_models/content/firmware.py +12 -5
  17. shepherd_core/data_models/content/firmware_datatype.py +7 -0
  18. shepherd_core/data_models/content/virtual_harvester.py +25 -20
  19. shepherd_core/data_models/content/virtual_harvester_fixture.yaml +1 -0
  20. shepherd_core/data_models/content/virtual_source.py +40 -23
  21. shepherd_core/data_models/content/virtual_source_fixture.yaml +1 -0
  22. shepherd_core/data_models/experiment/__init__.py +5 -4
  23. shepherd_core/data_models/experiment/experiment.py +16 -15
  24. shepherd_core/data_models/experiment/observer_features.py +18 -12
  25. shepherd_core/data_models/experiment/target_config.py +11 -7
  26. shepherd_core/data_models/readme.md +88 -0
  27. shepherd_core/data_models/task/__init__.py +10 -3
  28. shepherd_core/data_models/task/emulation.py +9 -6
  29. shepherd_core/data_models/task/firmware_mod.py +4 -2
  30. shepherd_core/data_models/task/harvest.py +5 -4
  31. shepherd_core/data_models/task/observer_tasks.py +4 -2
  32. shepherd_core/data_models/task/programming.py +3 -1
  33. shepherd_core/data_models/task/testbed_tasks.py +10 -4
  34. shepherd_core/data_models/testbed/__init__.py +5 -2
  35. shepherd_core/data_models/testbed/cape.py +8 -6
  36. shepherd_core/data_models/testbed/gpio.py +11 -9
  37. shepherd_core/data_models/testbed/mcu.py +10 -10
  38. shepherd_core/data_models/testbed/observer.py +10 -5
  39. shepherd_core/data_models/testbed/observer_fixture.yaml +23 -22
  40. shepherd_core/data_models/testbed/target.py +5 -3
  41. shepherd_core/data_models/testbed/target_fixture.yaml +11 -11
  42. shepherd_core/data_models/testbed/testbed.py +6 -3
  43. shepherd_core/decoder_waveform/__init__.py +2 -0
  44. shepherd_core/decoder_waveform/uart.py +44 -25
  45. shepherd_core/fw_tools/__init__.py +2 -0
  46. shepherd_core/fw_tools/converter.py +20 -9
  47. shepherd_core/fw_tools/converter_elf.py +3 -0
  48. shepherd_core/fw_tools/patcher.py +16 -4
  49. shepherd_core/fw_tools/validation.py +25 -5
  50. shepherd_core/inventory/__init__.py +66 -6
  51. shepherd_core/inventory/python.py +4 -0
  52. shepherd_core/inventory/system.py +13 -1
  53. shepherd_core/inventory/target.py +4 -0
  54. shepherd_core/logger.py +5 -0
  55. shepherd_core/reader.py +44 -26
  56. shepherd_core/testbed_client/__init__.py +2 -0
  57. shepherd_core/testbed_client/cache_path.py +17 -0
  58. shepherd_core/testbed_client/client.py +14 -8
  59. shepherd_core/testbed_client/fixtures.py +30 -11
  60. shepherd_core/testbed_client/user_model.py +13 -6
  61. shepherd_core/vsource/__init__.py +2 -0
  62. shepherd_core/vsource/virtual_converter_model.py +11 -4
  63. shepherd_core/vsource/virtual_harvester_model.py +8 -1
  64. shepherd_core/vsource/virtual_source_model.py +10 -5
  65. shepherd_core/writer.py +28 -20
  66. {shepherd_core-2023.12.1.dist-info → shepherd_core-2024.4.2.dist-info}/METADATA +50 -34
  67. shepherd_core-2024.4.2.dist-info/RECORD +75 -0
  68. {shepherd_core-2023.12.1.dist-info → shepherd_core-2024.4.2.dist-info}/WHEEL +1 -1
  69. {shepherd_core-2023.12.1.dist-info → shepherd_core-2024.4.2.dist-info}/top_level.txt +0 -1
  70. shepherd_core-2023.12.1.dist-info/RECORD +0 -117
  71. tests/__init__.py +0 -0
  72. tests/conftest.py +0 -64
  73. tests/data_models/__init__.py +0 -0
  74. tests/data_models/conftest.py +0 -14
  75. tests/data_models/example_cal_data.yaml +0 -31
  76. tests/data_models/example_cal_data_faulty.yaml +0 -29
  77. tests/data_models/example_cal_meas.yaml +0 -178
  78. tests/data_models/example_cal_meas_faulty1.yaml +0 -142
  79. tests/data_models/example_cal_meas_faulty2.yaml +0 -136
  80. tests/data_models/example_config_emulator.yaml +0 -41
  81. tests/data_models/example_config_experiment.yaml +0 -16
  82. tests/data_models/example_config_experiment_alternative.yaml +0 -14
  83. tests/data_models/example_config_harvester.yaml +0 -15
  84. tests/data_models/example_config_testbed.yaml +0 -26
  85. tests/data_models/example_config_virtsource.yaml +0 -78
  86. tests/data_models/test_base_models.py +0 -205
  87. tests/data_models/test_content_fixtures.py +0 -41
  88. tests/data_models/test_content_models.py +0 -282
  89. tests/data_models/test_examples.py +0 -48
  90. tests/data_models/test_experiment_models.py +0 -277
  91. tests/data_models/test_task_generation.py +0 -52
  92. tests/data_models/test_task_models.py +0 -131
  93. tests/data_models/test_testbed_fixtures.py +0 -47
  94. tests/data_models/test_testbed_models.py +0 -187
  95. tests/decoder_waveform/__init__.py +0 -0
  96. tests/decoder_waveform/test_decoder.py +0 -34
  97. tests/fw_tools/__init__.py +0 -0
  98. tests/fw_tools/conftest.py +0 -5
  99. tests/fw_tools/test_converter.py +0 -76
  100. tests/fw_tools/test_patcher.py +0 -66
  101. tests/fw_tools/test_validation.py +0 -56
  102. tests/inventory/__init__.py +0 -0
  103. tests/inventory/test_inventory.py +0 -20
  104. tests/test_cal_hw.py +0 -34
  105. tests/test_examples.py +0 -40
  106. tests/test_logger.py +0 -15
  107. tests/test_reader.py +0 -283
  108. tests/test_writer.py +0 -169
  109. tests/testbed_client/__init__.py +0 -0
  110. tests/vsource/__init__.py +0 -0
  111. tests/vsource/conftest.py +0 -49
  112. tests/vsource/test_converter.py +0 -161
  113. tests/vsource/test_harvester.py +0 -73
  114. tests/vsource/test_z.py +0 -5
  115. /shepherd_core/data_models/{doc_virtual_source.py → virtual_source_doc.txt} +0 -0
  116. {shepherd_core-2023.12.1.dist-info → shepherd_core-2024.4.2.dist-info}/zip-safe +0 -0
@@ -1,6 +1,10 @@
1
- """TODO: Work in Progress
1
+ """Helper-functions for firmware-validation.
2
2
 
3
+ TODO: Work in Progress.
4
+ - Each arch should have its own file and
5
+ - detection-functions that register in main validator.
3
6
  """
7
+
4
8
  import tempfile
5
9
  from pathlib import Path
6
10
 
@@ -26,6 +30,7 @@ except ImportError as e:
26
30
 
27
31
  @validate_call
28
32
  def is_hex(file: Path) -> bool:
33
+ """Check if file is containing intel-hex data."""
29
34
  try:
30
35
  _ = IntelHex(file.as_posix())
31
36
  except ValueError: # parsing
@@ -36,7 +41,9 @@ def is_hex(file: Path) -> bool:
36
41
 
37
42
 
38
43
  def is_hex_msp430(file: Path) -> bool:
39
- """Observations:
44
+ """Try to detect specifics for that MCU.
45
+
46
+ Observations:
40
47
  - addresses begin at 0x4000
41
48
  - value @0xFFFE (IVT) is start_address (of pgm-code)
42
49
  """
@@ -57,7 +64,9 @@ def is_hex_msp430(file: Path) -> bool:
57
64
 
58
65
 
59
66
  def is_hex_nrf52(file: Path) -> bool:
60
- """Observations:
67
+ """Try to detect specifics for that MCU.
68
+
69
+ Observations:
61
70
  - addresses begin at 0x0
62
71
  - only one segment (.get_segments), todo
63
72
  """
@@ -80,6 +89,7 @@ def is_hex_nrf52(file: Path) -> bool:
80
89
 
81
90
  @validate_call
82
91
  def is_elf(file: Path) -> bool:
92
+ """Check if file is an ELF file."""
83
93
  if ELF is None:
84
94
  raise RuntimeError(elf_error_text)
85
95
  if not file.is_file():
@@ -92,7 +102,11 @@ def is_elf(file: Path) -> bool:
92
102
  return True
93
103
 
94
104
 
95
- def is_elf_msp430(file: Path) -> bool: # TODO: allow detection without conversion
105
+ def is_elf_msp430(file: Path) -> bool:
106
+ """Check if file is an ELF for that MCU.
107
+
108
+ TODO: allow detection without conversion
109
+ """
96
110
  if is_elf(file):
97
111
  with tempfile.TemporaryDirectory() as path:
98
112
  file_hex = Path(path) / "file.hex"
@@ -103,7 +117,11 @@ def is_elf_msp430(file: Path) -> bool: # TODO: allow detection without conversi
103
117
  return False
104
118
 
105
119
 
106
- def is_elf_nrf52(file: Path) -> bool: # TODO: allow detection without conversion
120
+ def is_elf_nrf52(file: Path) -> bool:
121
+ """Check if file is an ELF for that MCU.
122
+
123
+ TODO: allow detection without conversion
124
+ """
107
125
  if is_elf(file):
108
126
  with tempfile.TemporaryDirectory() as path:
109
127
  file_hex = Path(path) / "file.hex"
@@ -115,6 +133,7 @@ def is_elf_nrf52(file: Path) -> bool: # TODO: allow detection without conversio
115
133
 
116
134
 
117
135
  def determine_type(file: Path) -> FirmwareDType:
136
+ """Figure out file-type (hex or elf)."""
118
137
  if not file.is_file():
119
138
  raise ValueError("Fn needs an existing file as input")
120
139
  if is_hex(file):
@@ -125,6 +144,7 @@ def determine_type(file: Path) -> FirmwareDType:
125
144
 
126
145
 
127
146
  def determine_arch(file: Path) -> str:
147
+ """Figure out arch (msp430 or nrf52)."""
128
148
  file_t = determine_type(file)
129
149
  if file_t == FirmwareDType.path_elf:
130
150
  if is_elf_nrf52(file):
@@ -1,8 +1,13 @@
1
- """Creates an overview for shepherd-host-machines with:
1
+ """Creates an overview for shepherd-host-machines.
2
+
3
+ This will collect:
2
4
  - relevant software-versions
3
5
  - system-parameters
4
- - hardware-config
6
+ - hardware-config.
5
7
  """
8
+
9
+ from datetime import datetime
10
+ from datetime import timedelta
6
11
  from pathlib import Path
7
12
  from typing import List
8
13
 
@@ -25,25 +30,39 @@ __all__ = [
25
30
 
26
31
 
27
32
  class Inventory(PythonInventory, SystemInventory, TargetInventory):
28
- # has all child-parameters
33
+ """Complete inventory for one device.
34
+
35
+ Has all child-parameters.
36
+ """
37
+
38
+ hostname: str
39
+ created: datetime
29
40
 
30
41
  @classmethod
31
42
  def collect(cls) -> Self:
32
43
  # one by one for more precise error messages
33
- pid = PythonInventory.collect().model_dump(exclude_unset=True, exclude_defaults=True)
44
+ # NOTE: system is first, as it must take a precise timestamp
34
45
  sid = SystemInventory.collect().model_dump(exclude_unset=True, exclude_defaults=True)
46
+ pid = PythonInventory.collect().model_dump(exclude_unset=True, exclude_defaults=True)
35
47
  tid = TargetInventory.collect().model_dump(exclude_unset=True, exclude_defaults=True)
36
48
  model = {**pid, **sid, **tid}
49
+ # make important metadata available at root level
50
+ model["created"] = sid["timestamp"]
51
+ model["hostname"] = sid["hostname"]
37
52
  return cls(**model)
38
53
 
39
54
 
40
55
  class InventoryList(ShpModel):
56
+ """Collection of inventories for several devices."""
57
+
41
58
  elements: Annotated[List[Inventory], Field(min_length=1)]
42
59
 
43
60
  def to_csv(self, path: Path) -> None:
44
- """TODO: pretty messed up (raw lists and dicts for sub-elements)
61
+ """Generate a CSV.
62
+
63
+ TODO: pretty messed up (raw lists and dicts for sub-elements)
45
64
  numpy.savetxt -> too basic
46
- np.concatenate(content).reshape((len(content), len(content[0])))
65
+ np.concatenate(content).reshape((len(content), len(content[0]))).
47
66
  """
48
67
  if path.is_dir():
49
68
  path = path / "inventory.yaml"
@@ -53,3 +72,44 @@ class InventoryList(ShpModel):
53
72
  content = list(item.model_dump().values())
54
73
  content = ["" if value is None else str(value) for value in content]
55
74
  fd.write(", ".join(content) + "\r\n")
75
+
76
+ def warn(self) -> dict:
77
+ warnings = {}
78
+ ts_earl = min([_e.created.timestamp() for _e in self.elements])
79
+ for _e in self.elements:
80
+ if _e.uptime > timedelta(hours=30).total_seconds():
81
+ warnings["uptime"] = f"[{self.hostname}] restart is recommended"
82
+ if (_e.created.timestamp() - ts_earl) > 10:
83
+ warnings["time_delta"] = f"[{self.hostname}] time-sync has failed"
84
+
85
+ # turn dict[hostname][type] = val
86
+ # to dict[type][val] = list[hostnames]
87
+ _inp = {
88
+ _e.hostname: _e.model_dump(exclude_unset=True, exclude_defaults=True)
89
+ for _e in self.elements
90
+ }
91
+ result = {}
92
+ for _host, _types in _inp.items():
93
+ for _type, _val in _types.items():
94
+ if _type not in result:
95
+ result[_type] = {}
96
+ if _val not in result[_type]:
97
+ result[_type][_val] = []
98
+ result[_type][_val].append(_host)
99
+ rescnt = {_key: len(_val) for _key, _val in result.items()}
100
+ t_unique = [
101
+ "h5py",
102
+ "numpy",
103
+ "pydantic",
104
+ "python",
105
+ "shepherd_core",
106
+ "shepherd_sheep",
107
+ "yaml",
108
+ "zstandard",
109
+ ]
110
+ for _key in t_unique:
111
+ if rescnt[_key] > 1:
112
+ warnings[_key] = f"[{_key}] VersionMismatch - {result[_key]}"
113
+
114
+ # TODO: finish with more potential warnings
115
+ return warnings
@@ -1,3 +1,5 @@
1
+ """Python related inventory model."""
2
+
1
3
  import platform
2
4
  from contextlib import suppress
3
5
  from importlib import import_module
@@ -10,6 +12,8 @@ from ..data_models import ShpModel
10
12
 
11
13
 
12
14
  class PythonInventory(ShpModel):
15
+ """Python related inventory model."""
16
+
13
17
  # program versions
14
18
  h5py: Optional[str] = None
15
19
  numpy: Optional[str] = None
@@ -1,12 +1,16 @@
1
+ """System / OS related inventory model."""
2
+
1
3
  import platform
2
4
  import subprocess
3
5
  import time
4
6
  from contextlib import suppress
7
+ from datetime import datetime
5
8
  from typing import Optional
6
9
 
7
10
  from typing_extensions import Self
8
11
 
9
- from .. import logger
12
+ from ..data_models.base.timezone import local_now
13
+ from ..logger import logger
10
14
 
11
15
  try:
12
16
  import psutil
@@ -20,8 +24,13 @@ from ..data_models import ShpModel
20
24
 
21
25
 
22
26
  class SystemInventory(ShpModel):
27
+ """System / OS related inventory model."""
28
+
23
29
  uptime: PositiveInt
24
30
  # ⤷ seconds
31
+ timestamp: datetime
32
+ # time_delta: timedelta = timedelta(0) # noqa: ERA001
33
+ # ⤷ lag behind earliest observer, TODO: wrong place
25
34
 
26
35
  system: str
27
36
  release: str
@@ -43,6 +52,8 @@ class SystemInventory(ShpModel):
43
52
 
44
53
  @classmethod
45
54
  def collect(cls) -> Self:
55
+ ts = local_now()
56
+
46
57
  if psutil is None:
47
58
  ifs2 = {}
48
59
  uptime = 0
@@ -58,6 +69,7 @@ class SystemInventory(ShpModel):
58
69
 
59
70
  model_dict = {
60
71
  "uptime": round(uptime),
72
+ "timestamp": ts,
61
73
  "system": platform.system(),
62
74
  "release": platform.release(),
63
75
  "version": platform.version(),
@@ -1,3 +1,5 @@
1
+ """Hardware related inventory model."""
2
+
1
3
  from typing import List
2
4
  from typing import Optional
3
5
 
@@ -8,6 +10,8 @@ from ..data_models import ShpModel
8
10
 
9
11
 
10
12
  class TargetInventory(ShpModel):
13
+ """Hardware related inventory model."""
14
+
11
15
  cape: Optional[str] = None
12
16
  targets: List[str] = [] # noqa: RUF012
13
17
 
shepherd_core/logger.py CHANGED
@@ -1,3 +1,5 @@
1
+ """Log handler of shepherd."""
2
+
1
3
  import logging
2
4
  import logging.handlers
3
5
  from typing import Union
@@ -12,10 +14,12 @@ verbose_level: int = 2
12
14
 
13
15
 
14
16
  def get_verbose_level() -> int:
17
+ """Get log level of shepherd."""
15
18
  return verbose_level
16
19
 
17
20
 
18
21
  def set_log_verbose_level(log_: Union[logging.Logger, logging.Handler], verbose: int) -> None:
22
+ """Set log level of shepherd."""
19
23
  if verbose == 0:
20
24
  log_.setLevel(logging.ERROR)
21
25
  logging.basicConfig(level=logging.ERROR)
@@ -39,6 +43,7 @@ def set_log_verbose_level(log_: Union[logging.Logger, logging.Handler], verbose:
39
43
 
40
44
 
41
45
  def increase_verbose_level(verbose: int) -> None:
46
+ """Increase log level of shepherd."""
42
47
  global verbose_level # noqa: PLW0603
43
48
  if verbose >= verbose_level:
44
49
  verbose_level = min(max(verbose, 0), 3)
shepherd_core/reader.py CHANGED
@@ -1,6 +1,7 @@
1
- """
2
- Reader-Baseclass
3
- """
1
+ """Reader-Baseclass."""
2
+
3
+ from __future__ import annotations
4
+
4
5
  import contextlib
5
6
  import errno
6
7
  import logging
@@ -8,7 +9,7 @@ import math
8
9
  import os
9
10
  from itertools import product
10
11
  from pathlib import Path
11
- from types import TracebackType
12
+ from typing import TYPE_CHECKING
12
13
  from typing import Any
13
14
  from typing import ClassVar
14
15
  from typing import Dict
@@ -31,13 +32,18 @@ from .data_models.base.calibration import CalibrationSeries
31
32
  from .data_models.content.energy_environment import EnergyDType
32
33
  from .decoder_waveform import Uart
33
34
 
35
+ if TYPE_CHECKING:
36
+ from types import TracebackType
37
+
34
38
 
35
39
  class Reader:
36
40
  """Sequentially Reads shepherd-data from HDF5 file.
37
41
 
38
42
  Args:
43
+ ----
39
44
  file_path: Path of hdf5 file containing shepherd data with iv-samples, iv-curves or isc&voc
40
45
  verbose: more debug-info during usage, 'None' skips the setter
46
+
41
47
  """
42
48
 
43
49
  samples_per_buffer: int = 10_000
@@ -95,7 +101,8 @@ class Reader:
95
101
  self.h5file = h5py.File(self.file_path, "r") # = readonly
96
102
  self._reader_opened = True
97
103
  except OSError as _xcp:
98
- raise TypeError(f"Unable to open HDF5-File '{self.file_path.name}'") from _xcp
104
+ msg = f"Unable to open HDF5-File '{self.file_path.name}'"
105
+ raise TypeError(msg) from _xcp
99
106
 
100
107
  if self.is_valid():
101
108
  self._logger.debug("File is available now")
@@ -158,7 +165,7 @@ class Reader:
158
165
  )
159
166
 
160
167
  def _refresh_file_stats(self) -> None:
161
- """update internal states, helpful after resampling or other changes in data-group"""
168
+ """Update internal states, helpful after resampling or other changes in data-group."""
162
169
  self.h5file.flush()
163
170
  if (self.ds_time.shape[0] > 1) and (self.ds_time[1] != self.ds_time[0]):
164
171
  # this assumes isochronal sampling
@@ -180,17 +187,20 @@ class Reader:
180
187
  is_raw: bool = False,
181
188
  omit_ts: bool = False,
182
189
  ) -> Generator[tuple, None, None]:
183
- """Generator that reads the specified range of buffers from the hdf5 file.
184
- can be configured on first call
185
- TODO: reconstruct - start/end mark samples and
186
- each call can request a certain number of samples
190
+ """Read the specified range of buffers from the hdf5 file.
191
+
192
+ Generator - can be configured on first call
193
+ TODO: reconstruct - start/end mark samples &
194
+ each call can request a certain number of samples.
187
195
 
188
196
  Args:
197
+ ----
189
198
  :param start_n: (int) Index of first buffer to be read
190
199
  :param end_n: (int) Index of last buffer to be read
191
200
  :param is_raw: (bool) output original data, not transformed to SI-Units
192
201
  :param omit_ts: (bool) optimize reading if timestamp is never used
193
202
  Yields: Buffers between start and end (tuple with time, voltage, current)
203
+
194
204
  """
195
205
  if end_n is None:
196
206
  end_n = int(self.ds_voltage.shape[0] // self.samples_per_buffer)
@@ -215,7 +225,7 @@ class Reader:
215
225
  )
216
226
 
217
227
  def get_calibration_data(self) -> CalibrationSeries:
218
- """Reads calibration-data from hdf5 file.
228
+ """Read calibration-data from hdf5 file.
219
229
 
220
230
  :return: Calibration data as CalibrationSeries object
221
231
  """
@@ -245,12 +255,14 @@ class Reader:
245
255
  try:
246
256
  if "datatype" in self.h5file["data"].attrs:
247
257
  return EnergyDType[self.h5file["data"].attrs["datatype"]]
248
- return None
249
258
  except KeyError:
250
259
  return None
260
+ else:
261
+ return None
251
262
 
252
263
  def get_hrv_config(self) -> dict:
253
- """essential info for harvester
264
+ """Essential info for harvester.
265
+
254
266
  :return: config-dict directly for vHarvester to be used during emulation
255
267
  """
256
268
  return {
@@ -259,7 +271,7 @@ class Reader:
259
271
  }
260
272
 
261
273
  def is_valid(self) -> bool:
262
- """checks file for plausibility
274
+ """Check file for plausibility, validity / correctness.
263
275
 
264
276
  :return: state of validity
265
277
  """
@@ -387,7 +399,7 @@ class Reader:
387
399
  return True
388
400
 
389
401
  def __getitem__(self, key: str) -> Any:
390
- """returns attribute or (if none found) a handle for a group or dataset (if found)
402
+ """Query attribute or (if none found) a handle for a group or dataset (if found).
391
403
 
392
404
  :param key: attribute, group, dataset
393
405
  :return: value of that key, or handle of object
@@ -399,9 +411,10 @@ class Reader:
399
411
  raise KeyError
400
412
 
401
413
  def energy(self) -> float:
402
- """determine the recorded energy of the trace
414
+ """Determine the recorded energy of the trace.
415
+
403
416
  # multiprocessing: https://stackoverflow.com/a/71898911
404
- # -> failed with multiprocessing.pool and pathos.multiprocessing.ProcessPool
417
+ # -> failed with multiprocessing.pool and pathos.multiprocessing.ProcessPool.
405
418
 
406
419
  :return: sampled energy in Ws (watt-seconds)
407
420
  """
@@ -427,7 +440,8 @@ class Reader:
427
440
  def _dset_statistics(
428
441
  self, dset: h5py.Dataset, cal: Optional[CalibrationPair] = None
429
442
  ) -> Dict[str, float]:
430
- """some basic stats for a provided dataset
443
+ """Create basic stats for a provided dataset.
444
+
431
445
  :param dset: dataset to evaluate
432
446
  :param cal: calibration (if wanted)
433
447
  :return: dict with entries for mean, min, max, std
@@ -469,8 +483,9 @@ class Reader:
469
483
  return stats
470
484
 
471
485
  def _data_timediffs(self) -> List[float]:
472
- """calculate list of (unique) time-deltas between buffers [s]
473
- -> optimized version that only looks at the start of each buffer
486
+ """Calculate list of unique time-deltas [s] between buffers.
487
+
488
+ Optimized version that only looks at the start of each buffer.
474
489
 
475
490
  :return: list of (unique) time-deltas between buffers [s]
476
491
  """
@@ -500,8 +515,9 @@ class Reader:
500
515
  return list(diffs)
501
516
 
502
517
  def check_timediffs(self) -> bool:
503
- """validate equal time-deltas
504
- -> unexpected time-jumps hint at a corrupted file or faulty measurement
518
+ """Validate equal time-deltas.
519
+
520
+ Unexpected time-jumps hint at a corrupted file or faulty measurement.
505
521
 
506
522
  :return: True if OK
507
523
  """
@@ -529,7 +545,8 @@ class Reader:
529
545
  *,
530
546
  minimal: bool = False,
531
547
  ) -> Dict[str, dict]:
532
- """recursive FN to capture the structure of the file
548
+ """Recursive FN to capture the structure of the file.
549
+
533
550
  :param node: starting node, leave free to go through whole file
534
551
  :param minimal: just provide a bare tree (much faster)
535
552
  :return: structure of that node with everything inside it
@@ -580,7 +597,7 @@ class Reader:
580
597
  return metadata
581
598
 
582
599
  def save_metadata(self, node: Union[h5py.Dataset, h5py.Group, None] = None) -> dict:
583
- """get structure of file and dump content to yaml-file with same name as original
600
+ """Get structure of file and dump content to yaml-file with same name as original.
584
601
 
585
602
  :param node: starting node, leave free to go through whole file
586
603
  :return: structure of that node with everything inside it
@@ -609,8 +626,9 @@ class Reader:
609
626
 
610
627
  @staticmethod
611
628
  def get_filter_for_redundant_states(data: np.ndarray) -> np.ndarray:
612
- """input is 1D state-vector, kep only first from identical & sequential states
613
- algo: create an offset-by-one vector and compare against original
629
+ """Input is 1D state-vector, kep only first from identical & sequential states.
630
+
631
+ Algo: create an offset-by-one vector and compare against original.
614
632
  """
615
633
  if len(data.shape) > 1:
616
634
  ValueError("Array must be 1D")
@@ -1,3 +1,5 @@
1
+ """Client to access a testbed-instance for controlling experiments."""
2
+
1
3
  from .client import tb_client
2
4
  from .user_model import User
3
5
 
@@ -0,0 +1,17 @@
1
+ """Generalized path for the file-cache."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+
7
+ def _get_xdg_path(variable_name: str, default_path: Path) -> Path:
8
+ _value = os.environ.get(variable_name)
9
+ if _value is None or _value == "":
10
+ return default_path
11
+ return Path(_value)
12
+
13
+
14
+ user_path = Path("~").expanduser()
15
+
16
+ cache_xdg_path = _get_xdg_path("XDG_CACHE_HOME", user_path / ".cache")
17
+ cache_user_path = cache_xdg_path / "shepherd_datalib"
@@ -1,3 +1,5 @@
1
+ """Client-Class to access a testbed instance."""
2
+
1
3
  from importlib import import_module
2
4
  from pathlib import Path
3
5
  from typing import Optional
@@ -16,6 +18,8 @@ from .user_model import User
16
18
 
17
19
 
18
20
  class TestbedClient:
21
+ """Client-Class to access a testbed instance."""
22
+
19
23
  _instance: Optional[Self] = None
20
24
 
21
25
  def __init__(self, server: Optional[str] = None, token: Union[str, Path, None] = None) -> None:
@@ -41,9 +45,10 @@ class TestbedClient:
41
45
 
42
46
  @validate_call
43
47
  def connect(self, server: Optional[str] = None, token: Union[str, Path, None] = None) -> bool:
44
- """
48
+ """Establish connection to testbed-server.
49
+
45
50
  server: either "local" to use demo-fixtures or something like "https://HOST:PORT"
46
- token: your account validation
51
+ token: your account validation.
47
52
  """
48
53
  if isinstance(token, Path):
49
54
  with token.resolve().open() as file:
@@ -78,19 +83,19 @@ class TestbedClient:
78
83
 
79
84
  def query_ids(self, model_type: str) -> list:
80
85
  if self._connected:
81
- raise RuntimeError("Not Implemented, TODO")
86
+ raise NotImplementedError("TODO")
82
87
  return list(self._fixtures[model_type].elements_by_id.keys())
83
88
 
84
89
  def query_names(self, model_type: str) -> list:
85
90
  if self._connected:
86
- raise RuntimeError("Not Implemented, TODO")
91
+ raise NotImplementedError("TODO")
87
92
  return list(self._fixtures[model_type].elements_by_name.keys())
88
93
 
89
94
  def query_item(
90
95
  self, model_type: str, uid: Optional[int] = None, name: Optional[str] = None
91
96
  ) -> dict:
92
97
  if self._connected:
93
- raise RuntimeError("Not Implemented, TODO")
98
+ raise NotImplementedError("TODO")
94
99
  if uid is not None:
95
100
  return self._fixtures[model_type].query_id(uid)
96
101
  if name is not None:
@@ -116,11 +121,11 @@ class TestbedClient:
116
121
 
117
122
  def try_inheritance(self, model_type: str, values: dict) -> (dict, list):
118
123
  if self._connected:
119
- raise RuntimeError("Not Implemented, TODO")
124
+ raise NotImplementedError("TODO")
120
125
  return self._fixtures[model_type].inheritance(values)
121
126
 
122
127
  def try_completing_model(self, model_type: str, values: dict) -> (dict, list):
123
- """init by name/id, for none existing instances raise Exception"""
128
+ """Init by name/id, for none existing instances raise Exception."""
124
129
  if len(values) == 1 and next(iter(values.keys())) in {"id", "name"}:
125
130
  value = next(iter(values.values()))
126
131
  if (
@@ -132,7 +137,8 @@ class TestbedClient:
132
137
  # TODO: still depending on _fixture
133
138
  values = self.query_item(model_type, uid=value)
134
139
  else:
135
- raise ValueError(f"Query {model_type} by name / ID failed - {values} is unknown!")
140
+ msg = f"Query {model_type} by name / ID failed - {values} is unknown!"
141
+ raise ValueError(msg)
136
142
  return self.try_inheritance(model_type, values)
137
143
 
138
144
  def fill_in_user_data(self, values: dict) -> dict: