shepherd-core 2025.5.2__py3-none-any.whl → 2025.6.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 (44) hide show
  1. shepherd_core/commons.py +3 -5
  2. shepherd_core/config.py +34 -0
  3. shepherd_core/data_models/__init__.py +2 -2
  4. shepherd_core/data_models/base/calibration.py +13 -8
  5. shepherd_core/data_models/base/content.py +4 -13
  6. shepherd_core/data_models/base/wrapper.py +4 -4
  7. shepherd_core/data_models/content/_external_fixtures.yaml +11 -11
  8. shepherd_core/data_models/content/energy_environment.py +1 -1
  9. shepherd_core/data_models/content/firmware.py +10 -5
  10. shepherd_core/data_models/content/virtual_harvester.py +256 -27
  11. shepherd_core/data_models/content/virtual_source.py +37 -28
  12. shepherd_core/data_models/content/virtual_source_fixture.yaml +1 -1
  13. shepherd_core/data_models/experiment/experiment.py +29 -19
  14. shepherd_core/data_models/experiment/observer_features.py +64 -28
  15. shepherd_core/data_models/experiment/target_config.py +19 -9
  16. shepherd_core/data_models/task/emulation.py +45 -32
  17. shepherd_core/data_models/task/firmware_mod.py +1 -1
  18. shepherd_core/data_models/task/harvest.py +16 -14
  19. shepherd_core/data_models/task/observer_tasks.py +8 -6
  20. shepherd_core/data_models/task/programming.py +3 -2
  21. shepherd_core/data_models/task/testbed_tasks.py +7 -9
  22. shepherd_core/data_models/testbed/cape_fixture.yaml +9 -1
  23. shepherd_core/data_models/testbed/gpio.py +7 -0
  24. shepherd_core/data_models/testbed/observer.py +1 -1
  25. shepherd_core/data_models/testbed/observer_fixture.yaml +19 -2
  26. shepherd_core/data_models/testbed/target.py +1 -1
  27. shepherd_core/data_models/testbed/target_fixture.old1 +1 -1
  28. shepherd_core/data_models/testbed/target_fixture.yaml +14 -1
  29. shepherd_core/data_models/testbed/testbed.py +8 -9
  30. shepherd_core/data_models/testbed/testbed_fixture.yaml +11 -0
  31. shepherd_core/fw_tools/patcher.py +7 -8
  32. shepherd_core/inventory/system.py +1 -3
  33. shepherd_core/reader.py +15 -7
  34. shepherd_core/testbed_client/cache_path.py +1 -1
  35. shepherd_core/testbed_client/client_web.py +2 -2
  36. shepherd_core/testbed_client/fixtures.py +13 -11
  37. shepherd_core/testbed_client/user_model.py +3 -6
  38. shepherd_core/version.py +1 -1
  39. shepherd_core/writer.py +2 -2
  40. {shepherd_core-2025.5.2.dist-info → shepherd_core-2025.6.1.dist-info}/METADATA +12 -12
  41. {shepherd_core-2025.5.2.dist-info → shepherd_core-2025.6.1.dist-info}/RECORD +44 -43
  42. {shepherd_core-2025.5.2.dist-info → shepherd_core-2025.6.1.dist-info}/WHEEL +1 -1
  43. {shepherd_core-2025.5.2.dist-info → shepherd_core-2025.6.1.dist-info}/top_level.txt +0 -0
  44. {shepherd_core-2025.5.2.dist-info → shepherd_core-2025.6.1.dist-info}/zip-safe +0 -0
@@ -34,7 +34,7 @@ class FirmwareModTask(ShpModel):
34
34
  firmware_file: Path
35
35
 
36
36
  verbose: Annotated[int, Field(ge=0, le=4)] = 2
37
- # ⤷ 0=Errors, 1=Warnings, 2=Info, 3=Debug
37
+ """ ⤷ 0=Errors, 1=Warnings, 2=Info, 3=Debug"""
38
38
 
39
39
  @model_validator(mode="after")
40
40
  def post_validation(self) -> Self:
@@ -9,6 +9,7 @@ from typing import Optional
9
9
  from pydantic import Field
10
10
  from pydantic import model_validator
11
11
  from typing_extensions import Self
12
+ from typing_extensions import deprecated
12
13
 
13
14
  from shepherd_core.data_models.base.shepherd import ShpModel
14
15
  from shepherd_core.data_models.base.timezone import local_tz
@@ -24,34 +25,35 @@ class HarvestTask(ShpModel):
24
25
 
25
26
  # General config
26
27
  output_path: Path
27
- # ⤷ dir- or file-path for storing the recorded data:
28
- # - providing a directory -> file is named hrv_timestamp.h5
29
- # - for a complete path the filename is not changed except it exists and
30
- # overwrite is disabled -> name#num.h5
28
+ """ ⤷ dir- or file-path for storing the recorded data:
29
+
30
+ - providing a directory -> file is named hrv_timestamp.h5
31
+ - for a complete path the filename is not changed except it exists and
32
+ overwrite is disabled -> name#num.h5
33
+ """
31
34
  force_overwrite: bool = False
32
- # ⤷ Overwrite existing file
35
+ """ ⤷ Overwrite existing file"""
33
36
  output_compression: Optional[Compression] = Compression.default
34
- # ⤷ should be 1 (level 1 gzip), lzf, or None (order of recommendation)
37
+ """ ⤷ should be 1 (level 1 gzip), lzf, or None (order of recommendation)"""
35
38
 
36
39
  time_start: Optional[datetime] = None
37
- # timestamp or unix epoch time, None = ASAP
40
+ """ timestamp or unix epoch time, None = ASAP"""
38
41
  duration: Optional[timedelta] = None
39
- # ⤷ Duration of recording in seconds, None = till EOF
40
- abort_on_error: bool = False
42
+ """ ⤷ Duration of recording in seconds, None = till EOFSys"""
43
+ abort_on_error: Annotated[bool, deprecated("has no effect")] = False
41
44
 
42
45
  # emulation-specific
43
46
  use_cal_default: bool = False
44
- # ⤷ Use default calibration values, skip loading from EEPROM
47
+ """ ⤷ Use default calibration values, skip loading from EEPROM"""
45
48
 
46
49
  virtual_harvester: VirtualHarvesterConfig = VirtualHarvesterConfig(name="mppt_opt")
47
- # ⤷ Choose one of the predefined virtual harvesters
48
- # or configure a new one
49
-
50
+ """ ⤷ Choose one of the predefined virtual harvesters or configure a new one
51
+ """
50
52
  power_tracing: PowerTracing = PowerTracing()
51
53
  sys_logging: Optional[SystemLogging] = SystemLogging()
52
54
 
53
55
  verbose: Annotated[int, Field(ge=0, le=4)] = 2
54
- # ⤷ 0=Errors, 1=Warnings, 2=Info, 3=Debug
56
+ """ ⤷ 0=Errors, 1=Warnings, 2=Info, 3=Debug"""
55
57
 
56
58
  # TODO: there is an unused DAC-Output patched to the harvesting-port
57
59
 
@@ -3,10 +3,12 @@
3
3
  from datetime import datetime
4
4
  from datetime import timedelta
5
5
  from pathlib import Path
6
+ from typing import Annotated
6
7
  from typing import Optional
7
8
 
8
9
  from pydantic import validate_call
9
10
  from typing_extensions import Self
11
+ from typing_extensions import deprecated
10
12
 
11
13
  from shepherd_core.data_models.base.content import IdInt
12
14
  from shepherd_core.data_models.base.content import NameStr
@@ -23,12 +25,10 @@ class ObserverTasks(ShpModel):
23
25
  """Collection of tasks for selected observer included in experiment."""
24
26
 
25
27
  observer: NameStr
26
- owner_id: Optional[IdInt] # TODO: set to optional for now, shouldn't be
27
28
 
28
29
  # PRE PROCESS
29
30
  time_prep: datetime # TODO: should be optional
30
31
  root_path: Path
31
- abort_on_error: bool
32
32
 
33
33
  # fw mod, store as hex-file and program
34
34
  fw1_mod: Optional[FirmwareModTask] = None
@@ -39,6 +39,10 @@ class ObserverTasks(ShpModel):
39
39
  # MAIN PROCESS
40
40
  emulation: Optional[EmulationTask] = None
41
41
 
42
+ # deprecations, TODO: remove before public release
43
+ owner_id: Annotated[Optional[IdInt], deprecated("not needed anymore")] = None
44
+ abort_on_error: Annotated[bool, deprecated("has no effect")] = False
45
+
42
46
  # post_copy / cleanup, Todo: could also just intake emuTask
43
47
  # - delete firmwares
44
48
  # - decode uart
@@ -67,10 +71,8 @@ class ObserverTasks(ShpModel):
67
71
 
68
72
  return cls(
69
73
  observer=obs.name,
70
- owner_id=xp.owner_id,
71
74
  time_prep=t_start - tb.prep_duration,
72
75
  root_path=root_path,
73
- abort_on_error=xp.abort_on_error,
74
76
  fw1_mod=FirmwareModTask.from_xp(xp, tb, tgt_id, 1, fw_paths[0]),
75
77
  fw2_mod=FirmwareModTask.from_xp(xp, tb, tgt_id, 2, fw_paths[1]),
76
78
  fw1_prog=ProgrammingTask.from_xp(xp, tb, tgt_id, 1, fw_paths[0]),
@@ -91,8 +93,8 @@ class ObserverTasks(ShpModel):
91
93
  tasks.append(task)
92
94
  return tasks
93
95
 
94
- def get_output_paths(self) -> dict:
95
- values = {}
96
+ def get_output_paths(self) -> dict[str, Path]:
97
+ values: dict[str, Path] = {}
96
98
  if isinstance(self.emulation, EmulationTask):
97
99
  if self.emulation.output_path is None:
98
100
  raise ValueError("Emu-Task should have a valid output-path")
@@ -29,15 +29,16 @@ class ProgrammingTask(ShpModel):
29
29
  target_port: TargetPort = TargetPort.A
30
30
  mcu_port: MCUPort = 1
31
31
  mcu_type: SafeStr
32
- # ⤷ must be either "nrf52" or "msp430" ATM, TODO: clean xp to tasks
32
+ """ ⤷ must be either "nrf52" or "msp430" ATM, TODO: clean xp to tasks"""
33
33
  voltage: Annotated[float, Field(ge=1, lt=5)] = 3
34
34
  datarate: Annotated[int, Field(gt=0, le=1_000_000)] = 200_000
35
35
  protocol: ProgrammerProtocol
36
+ # TODO: eradicate - should not exist. derive protocol from mcu_type
36
37
 
37
38
  simulate: bool = False
38
39
 
39
40
  verbose: Annotated[int, Field(ge=0, le=4)] = 2
40
- # ⤷ 0=Errors, 1=Warnings, 2=Info, 3=Debug
41
+ """ ⤷ 0=Errors, 1=Warnings, 2=Info, 3=Debug"""
41
42
 
42
43
  @model_validator(mode="after")
43
44
  def post_validation(self) -> Self:
@@ -1,11 +1,13 @@
1
1
  """Collection of tasks for all observers included in experiment."""
2
2
 
3
+ from pathlib import Path
3
4
  from typing import Annotated
4
5
  from typing import Optional
5
6
 
6
7
  from pydantic import Field
7
8
  from pydantic import validate_call
8
9
  from typing_extensions import Self
10
+ from typing_extensions import deprecated
9
11
 
10
12
  from shepherd_core.data_models.base.content import IdInt
11
13
  from shepherd_core.data_models.base.content import NameStr
@@ -22,11 +24,9 @@ class TestbedTasks(ShpModel):
22
24
  name: NameStr
23
25
  observer_tasks: Annotated[list[ObserverTasks], Field(min_length=1, max_length=128)]
24
26
 
25
- # POST PROCESS
26
- email_results: bool = False
27
- owner_id: Optional[IdInt]
28
- # TODO: had real email previously, does it really need these at all?
29
- # DB stores experiment and knows when to email
27
+ # deprecated, TODO: remove before public release
28
+ email_results: Annotated[Optional[bool], deprecated("not needed anymore")] = False
29
+ owner_id: Annotated[Optional[IdInt], deprecated("not needed anymore")] = None
30
30
 
31
31
  @classmethod
32
32
  @validate_call
@@ -40,8 +40,6 @@ class TestbedTasks(ShpModel):
40
40
  return cls(
41
41
  name=xp.name,
42
42
  observer_tasks=obs_tasks,
43
- email_results=xp.email_results,
44
- owner_id=xp.owner_id,
45
43
  )
46
44
 
47
45
  def get_observer_tasks(self, observer: str) -> Optional[ObserverTasks]:
@@ -50,11 +48,11 @@ class TestbedTasks(ShpModel):
50
48
  return tasks
51
49
  return None
52
50
 
53
- def get_output_paths(self) -> dict:
51
+ def get_output_paths(self) -> dict[str, Path]:
54
52
  # TODO: computed field preferred, but they don't work here, as
55
53
  # - they are always stored in yaml despite "repr=False"
56
54
  # - solution will shift to some kind of "result"-datatype that is combinable
57
- values = {}
55
+ values: dict[str, Path] = {}
58
56
  for obt in self.observer_tasks:
59
57
  values = {**values, **obt.get_output_paths()}
60
58
  return values
@@ -1,7 +1,7 @@
1
1
  ---
2
2
 
3
3
  # more human-readable test-protocol @
4
- # https://github.com/orgua/shepherd_v2_planning/blob/main/doc_testbed/Cape_pre-deployment-tests.xlsx
4
+ # https://github.com/orgua/shepherd-v2-planning/blob/main/doc_testbed/Cape_pre-deployment-tests.xlsx
5
5
  - datatype: cape
6
6
  parameters:
7
7
  id: 1270051
@@ -92,3 +92,11 @@
92
92
  id: 1270065
93
93
  name: cape65
94
94
  comment: only 220u/440uF on 5V
95
+ - datatype: cape
96
+ parameters:
97
+ id: 42000
98
+ name: unit_testing_cape
99
+ version: none
100
+ description: for unit testing
101
+ created: 2025-04-29
102
+ calibrated: 2025-04-29
@@ -65,3 +65,10 @@ class GPIO(ShpModel, title="GPIO of Observer Node"):
65
65
 
66
66
  def user_controllable(self) -> bool:
67
67
  return ("gpio" in self.name.lower()) and (self.direction in {"IO", "OUT"})
68
+
69
+ def user_recordable(self) -> bool:
70
+ return (
71
+ ("gpio" in self.name.lower())
72
+ and (self.direction in {"IO", "IN"})
73
+ and (self.pin_pru is not None)
74
+ )
@@ -43,7 +43,7 @@ class Observer(ShpModel, title="Shepherd-Sheep"):
43
43
 
44
44
  latitude: Annotated[float, Field(ge=-90, le=90)] = 51.026573
45
45
  longitude: Annotated[float, Field(ge=-180, le=180)] = 13.723291
46
- # ⤷ cfaed-floor
46
+ """ ⤷ cfaed-floor"""
47
47
 
48
48
  active: bool = True
49
49
  cape: Optional[Cape] = None
@@ -1,6 +1,6 @@
1
1
  ---
2
- # observer-cape-target-relation: https://github.com/orgua/shepherd_v2_planning/blob/main/doc_testbed/Cape_pre-deployment-tests.xlsx
3
- # network data: https://github.com/orgua/shepherd_v2_planning/blob/main/doc_testbed/ethernet_MAC_addresses_bbones.ods
2
+ # observer-cape-target-relation: https://github.com/orgua/shepherd-v2-planning/blob/main/doc_testbed/Cape_pre-deployment-tests.xlsx
3
+ # network data: https://github.com/orgua/shepherd-v2-planning/blob/main/doc_testbed/ethernet_MAC_addresses_bbones.ods
4
4
  - datatype: observer
5
5
  parameters:
6
6
  id: 0
@@ -221,3 +221,20 @@
221
221
  target_a:
222
222
  name: nRF52_FRAM_1392_390
223
223
  created: 2023-09-22 12:12:12
224
+ - datatype: observer
225
+ parameters:
226
+ id: 42
227
+ name: unit_testing_sheep
228
+ ip: 0.0.0.0
229
+ mac: 00:00:00:00:00:00
230
+ room: none
231
+ eth_port: none
232
+ description: unit testing
233
+ longitude: 0
234
+ latitude: 0
235
+ cape:
236
+ name: unit_testing_cape
237
+ target_a:
238
+ name: unit_testing_target
239
+ created: 2025-04-29 13:07:00
240
+ active: true
@@ -36,7 +36,7 @@ class Target(ShpModel, title="Target Node (DuT)"):
36
36
  created: datetime = Field(default_factory=datetime.now)
37
37
 
38
38
  testbed_id: Optional[IdInt16] = None
39
- # ⤷ is derived from ID (targets are still selected by id!)
39
+ """ ⤷ is derived from ID (targets are still selected by id!)"""
40
40
  mcu1: Union[MCU, NameStr]
41
41
  mcu2: Union[MCU, NameStr, None] = None
42
42
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  # more human-readable test-protocol @
3
- # https://github.com/orgua/shepherd_v2_planning/blob/main/doc_testbed/Target_pre-deployment-tests.xlsx
3
+ # https://github.com/orgua/shepherd-v2-planning/blob/main/doc_testbed/Target_pre-deployment-tests.xlsx
4
4
  - datatype: target
5
5
  parameters:
6
6
  id: 6 # Outer ID - selected by user for XP - can be rearranged
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  # more human-readable test-protocol @
3
- # https://github.com/orgua/shepherd_v2_planning/blob/main/doc_testbed/Target_pre-deployment-tests.xlsx
3
+ # https://github.com/orgua/shepherd-v2-planning/blob/main/doc_testbed/Target_pre-deployment-tests.xlsx
4
4
  - datatype: target
5
5
  parameters:
6
6
  id: 2 # Outer ID - selected by user for XP - can be rearranged
@@ -161,3 +161,16 @@
161
161
  name: nRF52
162
162
  mcu2: null
163
163
  created: 2022-12-12 12:12:12
164
+
165
+ - datatype: target
166
+ parameters:
167
+ id: 42
168
+ name: unit_testing_target
169
+ version: v0
170
+ description: for unit testing
171
+ comment: no comment
172
+ created: 2025-04-29
173
+ mcu1:
174
+ name: nRF52
175
+ mcu2:
176
+ name: MSP430FR
@@ -11,15 +11,17 @@ from pydantic import HttpUrl
11
11
  from pydantic import model_validator
12
12
  from typing_extensions import Self
13
13
 
14
+ from shepherd_core.config import config
14
15
  from shepherd_core.data_models.base.content import IdInt
15
16
  from shepherd_core.data_models.base.content import NameStr
16
17
  from shepherd_core.data_models.base.content import SafeStr
17
18
  from shepherd_core.data_models.base.shepherd import ShpModel
18
- from shepherd_core.logger import logger
19
19
  from shepherd_core.testbed_client import tb_client
20
20
 
21
21
  from .observer import Observer
22
22
 
23
+ duration_5min = timedelta(minutes=5)
24
+
23
25
 
24
26
  class Testbed(ShpModel):
25
27
  """meta-data representation of a testbed-component (physical object)."""
@@ -36,21 +38,18 @@ class Testbed(ShpModel):
36
38
  shared_storage: bool = True
37
39
  data_on_server: Path
38
40
  data_on_observer: Path
39
- # ⤷ storage layout: root_path/content_type/group/owner/[object]
41
+ """ ⤷ storage layout: root_path/content_type/group/owner/[object]"""
42
+ # TODO: we might need individual paths for experiments & content
40
43
 
41
- prep_duration: timedelta = timedelta(minutes=5)
44
+ prep_duration: timedelta = duration_5min
42
45
  # TODO: one BBone is currently time-keeper
43
46
 
44
47
  @model_validator(mode="before")
45
48
  @classmethod
46
49
  def query_database(cls, values: dict[str, Any]) -> dict[str, Any]:
47
- # allow instantiating an empty Testbed
48
- # -> query the first (and only) entry of client
50
+ # allow instantiating an empty Testbed, take default in config
49
51
  if len(values) == 0:
50
- ids = tb_client.query_ids(cls.__name__)
51
- if len(ids) > 1:
52
- logger.warning("More than one testbed defined?!?")
53
- values = {"id": ids[0]}
52
+ values = {"name": config.TESTBED}
54
53
 
55
54
  values, _ = tb_client.try_completing_model(cls.__name__, values)
56
55
  return values
@@ -23,3 +23,14 @@
23
23
  - name: sheep12
24
24
  - name: sheep13
25
25
  - name: sheep14
26
+
27
+ - datatype: testbed
28
+ parameters:
29
+ id: 42
30
+ name: unit_testing_testbed
31
+ description: "Artificial Testbed used in unit testing"
32
+ shared_storage: true
33
+ data_on_server: /tmp/
34
+ data_on_observer: /tmp/
35
+ observers:
36
+ - name: unit_testing_sheep
@@ -7,8 +7,7 @@ from typing import Optional
7
7
  from pydantic import Field
8
8
  from pydantic import validate_call
9
9
 
10
- from shepherd_core.commons import UID_NAME
11
- from shepherd_core.commons import UID_SIZE
10
+ from shepherd_core.config import config
12
11
  from shepherd_core.logger import logger
13
12
 
14
13
  from .validation import is_elf
@@ -50,7 +49,7 @@ def find_symbol(file_elf: Path, symbol: str) -> bool:
50
49
 
51
50
 
52
51
  @validate_call
53
- def read_symbol(file_elf: Path, symbol: str, length: int = UID_SIZE) -> Optional[int]:
52
+ def read_symbol(file_elf: Path, symbol: str, length: int) -> Optional[int]:
54
53
  """Read value of symbol in ELF-File.
55
54
 
56
55
  Will be interpreted as int.
@@ -68,7 +67,7 @@ def read_symbol(file_elf: Path, symbol: str, length: int = UID_SIZE) -> Optional
68
67
 
69
68
  def read_uid(file_elf: Path) -> Optional[int]:
70
69
  """Read value of UID-symbol for shepherd testbed."""
71
- return read_symbol(file_elf, symbol=UID_NAME, length=UID_SIZE)
70
+ return read_symbol(file_elf, symbol=config.UID_NAME, length=config.UID_SIZE)
72
71
 
73
72
 
74
73
  def read_arch(file_elf: Path) -> Optional[str]:
@@ -88,7 +87,7 @@ def read_arch(file_elf: Path) -> Optional[str]:
88
87
  def modify_symbol_value(
89
88
  file_elf: Path,
90
89
  symbol: str,
91
- value: Annotated[int, Field(ge=0, lt=2 ** (8 * UID_SIZE))],
90
+ value: Annotated[int, Field(ge=0, lt=2 ** (8 * config.UID_SIZE))],
92
91
  *,
93
92
  overwrite: bool = False,
94
93
  ) -> Optional[Path]:
@@ -106,10 +105,10 @@ def modify_symbol_value(
106
105
  raise RuntimeError(elf_error_text)
107
106
  elf = ELF(path=file_elf)
108
107
  addr = elf.symbols[symbol]
109
- value_raw = elf.read(address=addr, count=UID_SIZE)[-UID_SIZE:]
108
+ value_raw = elf.read(address=addr, count=config.UID_SIZE)[-config.UID_SIZE :]
110
109
  # ⤷ cutting needed -> msp produces 4b instead of 2
111
110
  value_old = int.from_bytes(bytes=value_raw, byteorder=elf.endian, signed=False)
112
- value_raw = value.to_bytes(length=UID_SIZE, byteorder=elf.endian, signed=False)
111
+ value_raw = value.to_bytes(length=config.UID_SIZE, byteorder=elf.endian, signed=False)
113
112
 
114
113
  try:
115
114
  elf.write(address=addr, data=value_raw)
@@ -132,4 +131,4 @@ def modify_symbol_value(
132
131
 
133
132
  def modify_uid(file_elf: Path, value: int) -> Optional[Path]:
134
133
  """Replace value of UID-symbol for shepherd testbed."""
135
- return modify_symbol_value(file_elf, symbol=UID_NAME, value=value, overwrite=True)
134
+ return modify_symbol_value(file_elf, symbol=config.UID_NAME, value=value, overwrite=True)
@@ -31,10 +31,8 @@ class SystemInventory(ShpModel):
31
31
  """System / OS related inventory model."""
32
32
 
33
33
  uptime: PositiveInt
34
- # ⤷ seconds
34
+ """ ⤷ seconds"""
35
35
  timestamp: datetime
36
- # time_delta: timedelta = timedelta(seconds=0) # noqa: ERA001
37
- # ⤷ lag behind earliest observer, TODO: wrong place
38
36
 
39
37
  system: str
40
38
  release: str
shepherd_core/reader.py CHANGED
@@ -7,6 +7,7 @@ import errno
7
7
  import logging
8
8
  import math
9
9
  import os
10
+ from datetime import datetime
10
11
  from itertools import product
11
12
  from pathlib import Path
12
13
  from types import MappingProxyType
@@ -24,9 +25,10 @@ from tqdm import trange
24
25
  from typing_extensions import Self
25
26
  from typing_extensions import deprecated
26
27
 
27
- from .commons import SAMPLERATE_SPS_DEFAULT
28
+ from .config import config
28
29
  from .data_models.base.calibration import CalibrationPair
29
30
  from .data_models.base.calibration import CalibrationSeries
31
+ from .data_models.base.timezone import local_tz
30
32
  from .data_models.content.energy_environment import EnergyDType
31
33
  from .decoder_waveform import Uart
32
34
 
@@ -75,7 +77,7 @@ class Reader:
75
77
  self._logger.setLevel(logging.DEBUG if verbose else logging.INFO)
76
78
 
77
79
  if not hasattr(self, "samplerate_sps"):
78
- self.samplerate_sps: int = SAMPLERATE_SPS_DEFAULT
80
+ self.samplerate_sps: int = config.SAMPLERATE_SPS
79
81
  self.sample_interval_ns: int = round(10**9 // self.samplerate_sps)
80
82
  self.sample_interval_s: float = 1 / self.samplerate_sps
81
83
 
@@ -263,6 +265,11 @@ class Reader:
263
265
  omit_timestamps=omit_ts,
264
266
  )
265
267
 
268
+ def get_time_start(self) -> Optional[datetime]:
269
+ if self.samples_n < 1:
270
+ return None
271
+ return datetime.fromtimestamp(self._cal.time.raw_to_si(self.ds_time[0]), tz=local_tz())
272
+
266
273
  def get_calibration_data(self) -> CalibrationSeries:
267
274
  """Read calibration-data from hdf5 file.
268
275
 
@@ -412,19 +419,20 @@ class Reader:
412
419
  self.file_path.name,
413
420
  )
414
421
  # same length of datasets:
415
- for dset in ["current", "time"]:
422
+ samples_n = self.h5file["data"]["time"].shape[0]
423
+ for dset in ["voltage", "current"]:
416
424
  ds_size = self.h5file["data"][dset].shape[0]
417
- if ds_size != self.samples_n:
425
+ if ds_size != samples_n:
418
426
  self._logger.warning(
419
427
  "[FileValidation] dataset '%s' has different size (=%d), "
420
- "compared to smallest set (=%d), in '%s'",
428
+ "compared to time (=%d), in '%s'",
421
429
  dset,
422
430
  ds_size,
423
- self.samples_n,
431
+ samples_n,
424
432
  self.file_path.name,
425
433
  )
426
434
  # dataset-length should be multiple of chunk-size
427
- remaining_size = self.samples_n % self.CHUNK_SAMPLES_N
435
+ remaining_size = samples_n % self.CHUNK_SAMPLES_N
428
436
  if remaining_size != 0:
429
437
  self._logger.warning(
430
438
  "[FileValidation] datasets are not aligned with chunk-size in '%s'",
@@ -14,4 +14,4 @@ def _get_xdg_path(variable_name: str, default_path: Path) -> Path:
14
14
  user_path = Path("~").expanduser()
15
15
 
16
16
  cache_xdg_path = _get_xdg_path("XDG_CACHE_HOME", user_path / ".cache")
17
- cache_user_path = cache_xdg_path / "shepherd_datalib"
17
+ cache_user_path = cache_xdg_path / "shepherd"
@@ -8,7 +8,7 @@ from typing import Union
8
8
 
9
9
  from pydantic import validate_call
10
10
 
11
- from shepherd_core.commons import TESTBED_SERVER_URI
11
+ from shepherd_core.config import config
12
12
  from shepherd_core.data_models.base.shepherd import ShpModel
13
13
  from shepherd_core.data_models.base.wrapper import Wrapper
14
14
 
@@ -38,7 +38,7 @@ class WebClient(AbcClient):
38
38
  if not hasattr(self, "_token"):
39
39
  # add default values
40
40
  self._token: str = "basic_public_access" # noqa: S105
41
- self._server: str = TESTBED_SERVER_URI
41
+ self._server: str = config.TESTBED_SERVER
42
42
  self._user: Optional[User] = None
43
43
  self._key: Optional[str] = None
44
44
  self._connected: bool = False
@@ -2,6 +2,7 @@
2
2
 
3
3
  import copy
4
4
  import pickle
5
+ from collections.abc import Iterable
5
6
  from collections.abc import Mapping
6
7
  from datetime import datetime
7
8
  from datetime import timedelta
@@ -34,11 +35,11 @@ class Fixture:
34
35
 
35
36
  def __init__(self, model_type: str) -> None:
36
37
  self.model_type: str = model_type.lower()
37
- self.elements_by_name: dict[str, dict] = {}
38
- self.elements_by_id: dict[int, dict] = {}
38
+ self.elements_by_name: dict[str, dict[str, Any]] = {}
39
+ self.elements_by_id: dict[int, dict[str, Any]] = {}
39
40
  # Iterator reset
40
41
  self._iter_index: int = 0
41
- self._iter_list: list = list(self.elements_by_name.values())
42
+ self._iter_list: list[dict[str, Any]] = list(self.elements_by_name.values())
42
43
 
43
44
  def insert(self, data: Wrapper) -> None:
44
45
  # ⤷ TODO: could get easier
@@ -54,9 +55,10 @@ class Fixture:
54
55
  self.elements_by_name[name] = data_model
55
56
  self.elements_by_id[_id] = data_model
56
57
  # update iterator
57
- self._iter_list = list(self.elements_by_name.values())
58
+ self._iter_list: list[dict[str, Any]] = list(self.elements_by_name.values())
58
59
 
59
- def __getitem__(self, key: Union[str, int]) -> dict:
60
+ def __getitem__(self, key: Union[str, int]) -> dict[str, Any]:
61
+ original_key = key
60
62
  if isinstance(key, str):
61
63
  key = key.lower()
62
64
  if key in self.elements_by_name:
@@ -65,7 +67,7 @@ class Fixture:
65
67
  key = int(key)
66
68
  if key in self.elements_by_id:
67
69
  return self.elements_by_id[int(key)]
68
- msg = f"{self.model_type} '{key}' not found!"
70
+ msg = f"{self.model_type} '{original_key}' not found!"
69
71
  raise ValueError(msg)
70
72
 
71
73
  def __iter__(self) -> Self:
@@ -73,14 +75,14 @@ class Fixture:
73
75
  self._iter_list = list(self.elements_by_name.values())
74
76
  return self
75
77
 
76
- def __next__(self) -> Any:
78
+ def __next__(self) -> dict[str, Any]:
77
79
  if self._iter_index < len(self._iter_list):
78
80
  member = self._iter_list[self._iter_index]
79
81
  self._iter_index += 1
80
82
  return member
81
83
  raise StopIteration
82
84
 
83
- def keys(self): # noqa: ANN201
85
+ def keys(self) -> Iterable[str]:
84
86
  return self.elements_by_name.keys()
85
87
 
86
88
  def refs(self) -> dict:
@@ -149,13 +151,13 @@ class Fixture:
149
151
  base[key] = value
150
152
  return base
151
153
 
152
- def query_id(self, _id: int) -> dict:
154
+ def query_id(self, _id: int) -> dict[str, Any]:
153
155
  if isinstance(_id, int) and _id in self.elements_by_id:
154
156
  return self.elements_by_id[_id]
155
157
  msg = f"Initialization of {self.model_type} by ID failed - {_id} is unknown!"
156
158
  raise ValueError(msg)
157
159
 
158
- def query_name(self, name: str) -> dict:
160
+ def query_name(self, name: str) -> dict[str, Any]:
159
161
  if isinstance(name, str) and name.lower() in self.elements_by_name:
160
162
  return self.elements_by_name[name.lower()]
161
163
  msg = f"Initialization of {self.model_type} by name failed - {name} is unknown!"
@@ -239,7 +241,7 @@ class Fixtures:
239
241
  msg = f"Component '{key}' not found!"
240
242
  raise ValueError(msg)
241
243
 
242
- def keys(self): # noqa: ANN201
244
+ def keys(self) -> Iterable[str]:
243
245
  return self.components.keys()
244
246
 
245
247
  @staticmethod
@@ -5,10 +5,7 @@ from hashlib import pbkdf2_hmac
5
5
  from typing import Annotated
6
6
  from typing import Any
7
7
  from typing import Optional
8
- from typing import Union
9
- from uuid import uuid4
10
8
 
11
- from pydantic import UUID4
12
9
  from pydantic import EmailStr
13
10
  from pydantic import Field
14
11
  from pydantic import SecretBytes
@@ -19,6 +16,7 @@ from pydantic import validate_call
19
16
 
20
17
  from shepherd_core.data_models.base.content import NameStr
21
18
  from shepherd_core.data_models.base.content import SafeStr
19
+ from shepherd_core.data_models.base.content import id_default
22
20
  from shepherd_core.data_models.base.shepherd import ShpModel
23
21
 
24
22
 
@@ -41,10 +39,9 @@ def hash_password(pw: Annotated[str, StringConstraints(min_length=20, max_length
41
39
  class User(ShpModel):
42
40
  """meta-data representation of a testbed-component (physical object)."""
43
41
 
44
- # id: UUID4 = Field( # TODO: db-migration - temp fix for documentation
45
- id: Union[UUID4, int] = Field(
42
+ id: int = Field(
46
43
  description="Unique ID",
47
- default_factory=uuid4,
44
+ default_factory=id_default,
48
45
  )
49
46
  name: NameStr
50
47
  description: Optional[SafeStr] = None
shepherd_core/version.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Separated string avoids circular imports."""
2
2
 
3
- version: str = "2025.05.2"
3
+ version: str = "2025.06.1"