shepherd-core 2025.4.1__py3-none-any.whl → 2025.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 (55) hide show
  1. shepherd_core/calibration_hw_def.py +11 -11
  2. shepherd_core/commons.py +4 -4
  3. shepherd_core/data_models/base/cal_measurement.py +10 -11
  4. shepherd_core/data_models/base/calibration.py +7 -6
  5. shepherd_core/data_models/base/content.py +1 -1
  6. shepherd_core/data_models/base/shepherd.py +6 -7
  7. shepherd_core/data_models/base/wrapper.py +2 -2
  8. shepherd_core/data_models/content/energy_environment.py +4 -3
  9. shepherd_core/data_models/content/firmware.py +9 -7
  10. shepherd_core/data_models/content/virtual_harvester.py +30 -22
  11. shepherd_core/data_models/content/virtual_source.py +16 -15
  12. shepherd_core/data_models/experiment/experiment.py +13 -13
  13. shepherd_core/data_models/experiment/observer_features.py +7 -8
  14. shepherd_core/data_models/experiment/target_config.py +12 -12
  15. shepherd_core/data_models/task/__init__.py +5 -5
  16. shepherd_core/data_models/task/emulation.py +13 -14
  17. shepherd_core/data_models/task/firmware_mod.py +11 -11
  18. shepherd_core/data_models/task/harvest.py +7 -6
  19. shepherd_core/data_models/task/observer_tasks.py +7 -7
  20. shepherd_core/data_models/task/programming.py +11 -11
  21. shepherd_core/data_models/task/testbed_tasks.py +8 -8
  22. shepherd_core/data_models/testbed/cape.py +7 -6
  23. shepherd_core/data_models/testbed/gpio.py +8 -7
  24. shepherd_core/data_models/testbed/mcu.py +8 -7
  25. shepherd_core/data_models/testbed/observer.py +9 -7
  26. shepherd_core/data_models/testbed/target.py +9 -7
  27. shepherd_core/data_models/testbed/testbed.py +11 -10
  28. shepherd_core/decoder_waveform/uart.py +5 -5
  29. shepherd_core/fw_tools/converter.py +4 -3
  30. shepherd_core/fw_tools/patcher.py +14 -15
  31. shepherd_core/fw_tools/validation.py +3 -2
  32. shepherd_core/inventory/__init__.py +6 -6
  33. shepherd_core/inventory/python.py +1 -1
  34. shepherd_core/inventory/system.py +11 -8
  35. shepherd_core/inventory/target.py +3 -3
  36. shepherd_core/logger.py +2 -2
  37. shepherd_core/reader.py +37 -39
  38. shepherd_core/testbed_client/client_abc_fix.py +20 -13
  39. shepherd_core/testbed_client/client_web.py +18 -11
  40. shepherd_core/testbed_client/fixtures.py +19 -16
  41. shepherd_core/testbed_client/user_model.py +6 -5
  42. shepherd_core/version.py +1 -1
  43. shepherd_core/vsource/target_model.py +3 -3
  44. shepherd_core/vsource/virtual_converter_model.py +3 -3
  45. shepherd_core/vsource/virtual_harvester_model.py +7 -9
  46. shepherd_core/vsource/virtual_harvester_simulation.py +6 -5
  47. shepherd_core/vsource/virtual_source_model.py +6 -5
  48. shepherd_core/vsource/virtual_source_simulation.py +7 -6
  49. shepherd_core/writer.py +32 -34
  50. {shepherd_core-2025.4.1.dist-info → shepherd_core-2025.4.2.dist-info}/METADATA +2 -3
  51. shepherd_core-2025.4.2.dist-info/RECORD +81 -0
  52. {shepherd_core-2025.4.1.dist-info → shepherd_core-2025.4.2.dist-info}/WHEEL +1 -1
  53. shepherd_core-2025.4.1.dist-info/RECORD +0 -81
  54. {shepherd_core-2025.4.1.dist-info → shepherd_core-2025.4.2.dist-info}/top_level.txt +0 -0
  55. {shepherd_core-2025.4.1.dist-info → shepherd_core-2025.4.2.dist-info}/zip-safe +0 -0
@@ -8,7 +8,7 @@ from typing import Optional
8
8
  from pydantic import ConfigDict
9
9
  from typing_extensions import Self
10
10
 
11
- from ..data_models import ShpModel
11
+ from shepherd_core.data_models import ShpModel
12
12
 
13
13
 
14
14
  class PythonInventory(ShpModel):
@@ -3,15 +3,18 @@
3
3
  import platform
4
4
  import subprocess
5
5
  import time
6
+ from collections.abc import Mapping
7
+ from collections.abc import Sequence
6
8
  from datetime import datetime
7
9
  from pathlib import Path
8
- from typing import List
10
+ from types import MappingProxyType
11
+ from typing import Any
9
12
  from typing import Optional
10
13
 
11
14
  from typing_extensions import Self
12
15
 
13
- from ..data_models.base.timezone import local_now
14
- from ..logger import logger
16
+ from shepherd_core.data_models.base.timezone import local_now
17
+ from shepherd_core.logger import logger
15
18
 
16
19
  try:
17
20
  import psutil
@@ -21,7 +24,7 @@ except ImportError:
21
24
  from pydantic import ConfigDict
22
25
  from pydantic.types import PositiveInt
23
26
 
24
- from ..data_models import ShpModel
27
+ from shepherd_core.data_models import ShpModel
25
28
 
26
29
 
27
30
  class SystemInventory(ShpModel):
@@ -30,7 +33,7 @@ class SystemInventory(ShpModel):
30
33
  uptime: PositiveInt
31
34
  # ⤷ seconds
32
35
  timestamp: datetime
33
- # time_delta: timedelta = timedelta(0) # noqa: ERA001
36
+ # time_delta: timedelta = timedelta(seconds=0) # noqa: ERA001
34
37
  # ⤷ lag behind earliest observer, TODO: wrong place
35
38
 
36
39
  system: str
@@ -44,13 +47,13 @@ class SystemInventory(ShpModel):
44
47
 
45
48
  hostname: str
46
49
 
47
- interfaces: dict = {} # noqa: RUF012
50
+ interfaces: Mapping[str, Any] = MappingProxyType({})
48
51
  # ⤷ tuple with
49
52
  # ip IPvAnyAddress
50
53
  # mac MACStr
51
54
 
52
- fs_root: List[str] = None
53
- beagle: List[str] = None
55
+ fs_root: Sequence[str] = ()
56
+ beagle: Sequence[str] = ()
54
57
 
55
58
  model_config = ConfigDict(str_min_length=0)
56
59
 
@@ -1,19 +1,19 @@
1
1
  """Hardware related inventory model."""
2
2
 
3
- from typing import List
3
+ from collections.abc import Sequence
4
4
  from typing import Optional
5
5
 
6
6
  from pydantic import ConfigDict
7
7
  from typing_extensions import Self
8
8
 
9
- from ..data_models import ShpModel
9
+ from shepherd_core.data_models import ShpModel
10
10
 
11
11
 
12
12
  class TargetInventory(ShpModel):
13
13
  """Hardware related inventory model."""
14
14
 
15
15
  cape: Optional[str] = None
16
- targets: List[str] = [] # noqa: RUF012
16
+ targets: Sequence[str] = ()
17
17
 
18
18
  model_config = ConfigDict(str_min_length=0)
19
19
 
shepherd_core/logger.py CHANGED
@@ -33,8 +33,8 @@ def set_log_verbose_level(log_: Union[logging.Logger, logging.Handler], verbose:
33
33
  if verbose < 3:
34
34
  # reduce log-overhead when not debugging, also more user-friendly exceptions
35
35
  logging._srcfile = None # noqa: SLF001
36
- logging.logThreads = 0
37
- logging.logProcesses = 0
36
+ logging.logThreads = False
37
+ logging.logProcesses = False
38
38
 
39
39
  if verbose > 2:
40
40
  chromalog.basicConfig(format="%(name)s %(levelname)s: %(message)s")
shepherd_core/reader.py CHANGED
@@ -9,14 +9,10 @@ import math
9
9
  import os
10
10
  from itertools import product
11
11
  from pathlib import Path
12
+ from types import MappingProxyType
12
13
  from typing import TYPE_CHECKING
13
14
  from typing import Any
14
- from typing import ClassVar
15
- from typing import Dict
16
- from typing import Generator
17
- from typing import List
18
15
  from typing import Optional
19
- from typing import Type
20
16
  from typing import Union
21
17
 
22
18
  import h5py
@@ -26,13 +22,16 @@ from pydantic import validate_call
26
22
  from tqdm import trange
27
23
  from typing_extensions import Self
28
24
 
29
- from .commons import samplerate_sps_default
25
+ from .commons import SAMPLERATE_SPS_DEFAULT
30
26
  from .data_models.base.calibration import CalibrationPair
31
27
  from .data_models.base.calibration import CalibrationSeries
32
28
  from .data_models.content.energy_environment import EnergyDType
33
29
  from .decoder_waveform import Uart
34
30
 
35
31
  if TYPE_CHECKING:
32
+ from collections.abc import Generator
33
+ from collections.abc import Mapping
34
+ from collections.abc import Sequence
36
35
  from types import TracebackType
37
36
 
38
37
 
@@ -46,28 +45,27 @@ class Reader:
46
45
 
47
46
  """
48
47
 
49
- samples_per_buffer: int = 10_000
48
+ BUFFER_SAMPLES_N: int = 10_000
50
49
 
51
- mode_dtype_dict: ClassVar[dict] = {
52
- "harvester": [
53
- EnergyDType.ivsample,
54
- EnergyDType.ivcurve,
55
- EnergyDType.isc_voc,
56
- ],
57
- "emulator": [EnergyDType.ivsample],
58
- }
50
+ MODE_TO_DTYPE: Mapping[str, Sequence[EnergyDType]] = MappingProxyType(
51
+ {
52
+ "harvester": (
53
+ EnergyDType.ivsample,
54
+ EnergyDType.ivcurve,
55
+ EnergyDType.isc_voc,
56
+ ),
57
+ "emulator": (EnergyDType.ivsample,),
58
+ }
59
+ )
59
60
 
60
61
  @validate_call
61
62
  def __init__(
62
63
  self,
63
- file_path: Optional[Path],
64
+ file_path: Path,
64
65
  *,
65
- verbose: Optional[bool] = True,
66
+ verbose: bool = True,
66
67
  ) -> None:
67
- if not hasattr(self, "file_path"):
68
- self.file_path: Optional[Path] = None
69
- if isinstance(file_path, (Path, str)):
70
- self.file_path = Path(file_path).resolve()
68
+ self.file_path: Path = file_path.resolve()
71
69
 
72
70
  if not hasattr(self, "_logger"):
73
71
  self._logger: logging.Logger = logging.getLogger("SHPCore.Reader")
@@ -75,7 +73,7 @@ class Reader:
75
73
  self._logger.setLevel(logging.DEBUG if verbose else logging.INFO)
76
74
 
77
75
  if not hasattr(self, "samplerate_sps"):
78
- self.samplerate_sps: int = samplerate_sps_default
76
+ self.samplerate_sps: int = SAMPLERATE_SPS_DEFAULT
79
77
  self.sample_interval_ns: int = round(10**9 // self.samplerate_sps)
80
78
  self.sample_interval_s: float = 1 / self.samplerate_sps
81
79
 
@@ -155,7 +153,7 @@ class Reader:
155
153
 
156
154
  def __exit__(
157
155
  self,
158
- typ: Optional[Type[BaseException]] = None,
156
+ typ: Optional[type[BaseException]] = None,
159
157
  exc: Optional[BaseException] = None,
160
158
  tb: Optional[TracebackType] = None,
161
159
  extra_arg: int = 0,
@@ -183,7 +181,7 @@ class Reader:
183
181
  self.sample_interval_ns = round(10**9 * self.sample_interval_s)
184
182
  self.samplerate_sps = max(round((sample_count - 1) / duration_s), 1)
185
183
  self.runtime_s = round(self.ds_voltage.shape[0] / self.samplerate_sps, 1)
186
- self.buffers_n = int(self.ds_voltage.shape[0] // self.samples_per_buffer)
184
+ self.buffers_n = int(self.ds_voltage.shape[0] // self.BUFFER_SAMPLES_N)
187
185
  if isinstance(self.file_path, Path):
188
186
  self.file_size = self.file_path.stat().st_size
189
187
  else:
@@ -214,7 +212,7 @@ class Reader:
214
212
 
215
213
  """
216
214
  if n_samples_per_buffer is None:
217
- n_samples_per_buffer = self.samples_per_buffer
215
+ n_samples_per_buffer = self.BUFFER_SAMPLES_N
218
216
  end_max = int(self.ds_voltage.shape[0] // n_samples_per_buffer)
219
217
  end_n = end_max if end_n is None else min(end_n, end_max)
220
218
  self._logger.debug("Reading blocks %d to %d from source-file", start_n, end_n)
@@ -254,7 +252,7 @@ class Reader:
254
252
  return self.h5file.attrs["mode"]
255
253
  return ""
256
254
 
257
- def get_config(self) -> Dict:
255
+ def get_config(self) -> dict:
258
256
  if "config" in self.h5file["data"].attrs:
259
257
  return yaml.safe_load(self.h5file["data"].attrs["config"])
260
258
  return {}
@@ -329,7 +327,7 @@ class Reader:
329
327
  self.file_path.name,
330
328
  )
331
329
  return False
332
- if self.h5file.attrs["mode"] not in self.mode_dtype_dict:
330
+ if self.h5file.attrs["mode"] not in self.MODE_TO_DTYPE:
333
331
  self._logger.error(
334
332
  "[FileValidation] unsupported mode '%s' in '%s'",
335
333
  attr,
@@ -361,7 +359,7 @@ class Reader:
361
359
  self.file_path.name,
362
360
  )
363
361
  return False
364
- if self.get_datatype() not in self.mode_dtype_dict[self.get_mode()]:
362
+ if self.get_datatype() not in self.MODE_TO_DTYPE[self.get_mode()]:
365
363
  self._logger.error(
366
364
  "[FileValidation] unsupported type '%s' for mode '%s' in '%s'",
367
365
  self.get_datatype(),
@@ -399,7 +397,7 @@ class Reader:
399
397
  self.file_path.name,
400
398
  )
401
399
  # dataset-length should be multiple of buffersize
402
- remaining_size = ds_volt_size % self.samples_per_buffer
400
+ remaining_size = ds_volt_size % self.BUFFER_SAMPLES_N
403
401
  if remaining_size != 0:
404
402
  self._logger.warning(
405
403
  "[FileValidation] datasets are not aligned with buffer-size in '%s'",
@@ -478,7 +476,7 @@ class Reader:
478
476
 
479
477
  def _dset_statistics(
480
478
  self, dset: h5py.Dataset, cal: Optional[CalibrationPair] = None
481
- ) -> Dict[str, float]:
479
+ ) -> dict[str, float]:
482
480
  """Create basic stats for a provided dataset.
483
481
 
484
482
  :param dset: dataset to evaluate
@@ -511,7 +509,7 @@ class Reader:
511
509
  if len(stats_list) < 1:
512
510
  return {}
513
511
  stats_nd = np.stack(stats_list)
514
- stats: Dict[str, float] = {
512
+ stats: dict[str, float] = {
515
513
  # TODO: wrong calculation for ndim-datasets with n>1
516
514
  "mean": float(stats_nd[:, 0].mean()),
517
515
  "min": float(stats_nd[:, 1].min()),
@@ -521,7 +519,7 @@ class Reader:
521
519
  }
522
520
  return stats
523
521
 
524
- def _data_timediffs(self) -> List[float]:
522
+ def _data_timediffs(self) -> list[float]:
525
523
  """Calculate list of unique time-deltas [s] between buffers.
526
524
 
527
525
  Optimized version that only looks at the start of each buffer.
@@ -540,14 +538,14 @@ class Reader:
540
538
 
541
539
  def calc_timediffs(idx_start: int) -> list:
542
540
  ds_time = self.ds_time[
543
- idx_start : (idx_start + self.max_elements) : self.samples_per_buffer
541
+ idx_start : (idx_start + self.max_elements) : self.BUFFER_SAMPLES_N
544
542
  ]
545
543
  diffs_np = np.unique(ds_time[1:] - ds_time[0:-1], return_counts=False)
546
544
  return list(np.array(diffs_np))
547
545
 
548
546
  diffs_ll = [calc_timediffs(i) for i in job_iter]
549
547
  diffs = {
550
- round(self._cal.time.raw_to_si(j) / self.samples_per_buffer, 6)
548
+ round(self._cal.time.raw_to_si(j) / self.BUFFER_SAMPLES_N, 6)
551
549
  for i in diffs_ll
552
550
  for j in i
553
551
  }
@@ -565,7 +563,7 @@ class Reader:
565
563
  self._logger.warning(
566
564
  "Time-jumps detected -> expected equal steps, but got: %s s", diffs
567
565
  )
568
- return (len(diffs) <= 1) and diffs[0] == round(0.1 / self.samples_per_buffer, 6)
566
+ return (len(diffs) <= 1) and diffs[0] == round(0.1 / self.BUFFER_SAMPLES_N, 6)
569
567
 
570
568
  def count_errors_in_log(self, group_name: str = "sheep", min_level: int = 40) -> int:
571
569
  if group_name not in self.h5file:
@@ -583,7 +581,7 @@ class Reader:
583
581
  node: Union[h5py.Dataset, h5py.Group, None] = None,
584
582
  *,
585
583
  minimal: bool = False,
586
- ) -> Dict[str, dict]:
584
+ ) -> dict[str, dict]:
587
585
  """Recursive FN to capture the structure of the file.
588
586
 
589
587
  :param node: starting node, leave free to go through whole file
@@ -594,7 +592,7 @@ class Reader:
594
592
  self._refresh_file_stats()
595
593
  return self.get_metadata(self.h5file, minimal=minimal)
596
594
 
597
- metadata: Dict[str, dict] = {}
595
+ metadata: dict[str, dict] = {}
598
596
  if isinstance(node, h5py.Dataset) and not minimal:
599
597
  metadata["_dataset_info"] = {
600
598
  "datatype": str(node.dtype),
@@ -616,7 +614,7 @@ class Reader:
616
614
  with contextlib.suppress(yaml.YAMLError):
617
615
  attr_value = yaml.safe_load(attr_value)
618
616
  elif "int" in str(type(attr_value)):
619
- # TODO: why not isinstance? can it be List[int] other complex type?
617
+ # TODO: why not isinstance? can it be list[int] other complex type?
620
618
  attr_value = int(attr_value)
621
619
  else:
622
620
  attr_value = float(attr_value)
@@ -675,7 +673,7 @@ class Reader:
675
673
  return data != data_1
676
674
 
677
675
  def gpio_to_waveforms(self, name: Optional[str] = None) -> dict:
678
- waveforms: Dict[str, np.ndarray] = {}
676
+ waveforms: dict[str, np.ndarray] = {}
679
677
  if "gpio" not in self.h5file:
680
678
  return waveforms
681
679
 
@@ -17,11 +17,12 @@ TODO: Comfort functions missing
17
17
 
18
18
  from abc import ABC
19
19
  from abc import abstractmethod
20
- from typing import List
20
+ from typing import Any
21
21
  from typing import Optional
22
22
 
23
- from ..data_models.base.shepherd import ShpModel
24
- from ..data_models.base.wrapper import Wrapper
23
+ from shepherd_core.data_models.base.shepherd import ShpModel
24
+ from shepherd_core.data_models.base.wrapper import Wrapper
25
+
25
26
  from .fixtures import Fixtures
26
27
 
27
28
 
@@ -40,11 +41,11 @@ class AbcClient(ABC):
40
41
  """
41
42
 
42
43
  @abstractmethod
43
- def query_ids(self, model_type: str) -> List[int]:
44
+ def query_ids(self, model_type: str) -> list[int]:
44
45
  pass
45
46
 
46
47
  @abstractmethod
47
- def query_names(self, model_type: str) -> List[str]:
48
+ def query_names(self, model_type: str) -> list[str]:
48
49
  pass
49
50
 
50
51
  @abstractmethod
@@ -54,11 +55,15 @@ class AbcClient(ABC):
54
55
  pass
55
56
 
56
57
  @abstractmethod
57
- def try_inheritance(self, model_type: str, values: dict) -> (dict, list):
58
+ def try_inheritance(
59
+ self, model_type: str, values: dict[str, Any]
60
+ ) -> tuple[dict[str, Any], list[str]]:
58
61
  # TODO: maybe internal? yes
59
62
  pass
60
63
 
61
- def try_completing_model(self, model_type: str, values: dict) -> (dict, list):
64
+ def try_completing_model(
65
+ self, model_type: str, values: dict[str, Any]
66
+ ) -> tuple[dict[str, Any], list[str]]:
62
67
  """Init by name/id, for none existing instances raise Exception.
63
68
 
64
69
  This is the main entry-point for querying a model (used be the core-lib).
@@ -73,7 +78,7 @@ class AbcClient(ABC):
73
78
  return self.try_inheritance(model_type, values)
74
79
 
75
80
  @abstractmethod
76
- def fill_in_user_data(self, values: dict) -> dict:
81
+ def fill_in_user_data(self, values: dict[str, Any]) -> dict[str, Any]:
77
82
  # TODO: is it really helpful and needed?
78
83
  pass
79
84
 
@@ -83,7 +88,7 @@ class FixturesClient(AbcClient):
83
88
 
84
89
  def __init__(self) -> None:
85
90
  super().__init__()
86
- self._fixtures: Optional[Fixtures] = Fixtures()
91
+ self._fixtures: Fixtures = Fixtures()
87
92
 
88
93
  def insert(self, data: ShpModel) -> bool:
89
94
  wrap = Wrapper(
@@ -93,10 +98,10 @@ class FixturesClient(AbcClient):
93
98
  self._fixtures.insert_model(wrap)
94
99
  return True
95
100
 
96
- def query_ids(self, model_type: str) -> List[int]:
101
+ def query_ids(self, model_type: str) -> list[int]:
97
102
  return list(self._fixtures[model_type].elements_by_id.keys())
98
103
 
99
- def query_names(self, model_type: str) -> List[str]:
104
+ def query_names(self, model_type: str) -> list[str]:
100
105
  return list(self._fixtures[model_type].elements_by_name.keys())
101
106
 
102
107
  def query_item(
@@ -108,10 +113,12 @@ class FixturesClient(AbcClient):
108
113
  return self._fixtures[model_type].query_name(name)
109
114
  raise ValueError("Query needs either uid or name of object")
110
115
 
111
- def try_inheritance(self, model_type: str, values: dict) -> (dict, list):
116
+ def try_inheritance(
117
+ self, model_type: str, values: dict[str, Any]
118
+ ) -> tuple[dict[str, Any], list[str]]:
112
119
  return self._fixtures[model_type].inheritance(values)
113
120
 
114
- def fill_in_user_data(self, values: dict) -> dict:
121
+ def fill_in_user_data(self, values: dict[str, Any]) -> dict[str, Any]:
115
122
  """Add fake user-data when offline-client is used.
116
123
 
117
124
  Hotfix until WebClient is working.
@@ -2,15 +2,16 @@
2
2
 
3
3
  from importlib import import_module
4
4
  from pathlib import Path
5
- from typing import List
5
+ from typing import Any
6
6
  from typing import Optional
7
7
  from typing import Union
8
8
 
9
9
  from pydantic import validate_call
10
10
 
11
- from ..commons import testbed_server_default
12
- from ..data_models.base.shepherd import ShpModel
13
- from ..data_models.base.wrapper import Wrapper
11
+ from shepherd_core.commons import TESTBED_SERVER_URI
12
+ from shepherd_core.data_models.base.shepherd import ShpModel
13
+ from shepherd_core.data_models.base.wrapper import Wrapper
14
+
14
15
  from .client_abc_fix import AbcClient
15
16
  from .user_model import User
16
17
 
@@ -37,7 +38,7 @@ class WebClient(AbcClient):
37
38
  if not hasattr(self, "_token"):
38
39
  # add default values
39
40
  self._token: str = "basic_public_access" # noqa: S105
40
- self._server: str = testbed_server_default
41
+ self._server: str = TESTBED_SERVER_URI
41
42
  self._user: Optional[User] = None
42
43
  self._key: Optional[str] = None
43
44
  self._connected: bool = False
@@ -49,6 +50,8 @@ class WebClient(AbcClient):
49
50
  # ABC Functions below
50
51
 
51
52
  def insert(self, data: ShpModel) -> bool:
53
+ if self._req is None:
54
+ return False
52
55
  wrap = Wrapper(
53
56
  datatype=type(data).__name__,
54
57
  parameters=data.model_dump(),
@@ -57,10 +60,10 @@ class WebClient(AbcClient):
57
60
  r.raise_for_status()
58
61
  return True
59
62
 
60
- def query_ids(self, model_type: str) -> List[int]:
63
+ def query_ids(self, model_type: str) -> list[int]:
61
64
  raise NotImplementedError("TODO")
62
65
 
63
- def query_names(self, model_type: str) -> List[str]:
66
+ def query_names(self, model_type: str) -> list[str]:
64
67
  raise NotImplementedError("TODO")
65
68
 
66
69
  def query_item(
@@ -68,10 +71,14 @@ class WebClient(AbcClient):
68
71
  ) -> dict:
69
72
  raise NotImplementedError("TODO")
70
73
 
71
- def try_inheritance(self, model_type: str, values: dict) -> (dict, list):
74
+ def try_inheritance(
75
+ self, model_type: str, values: dict[str, Any]
76
+ ) -> tuple[dict[str, Any], list[str]]:
72
77
  raise NotImplementedError("TODO")
73
78
 
74
- def fill_in_user_data(self, values: dict) -> dict:
79
+ def fill_in_user_data(self, values: dict[str, Any]) -> dict[str, Any]:
80
+ if self._user is None:
81
+ return values
75
82
  if values.get("owner") is None:
76
83
  values["owner"] = self._user.name
77
84
  if values.get("group") is None:
@@ -105,7 +112,7 @@ class WebClient(AbcClient):
105
112
  return self._query_user_data()
106
113
 
107
114
  def _query_session_key(self) -> bool:
108
- if self._server:
115
+ if self._server and self._req is not None:
109
116
  r = self._req.get(self._server + "/session_key", timeout=2)
110
117
  r.raise_for_status()
111
118
  self._key = r.json()["value"] # TODO: not finished
@@ -113,7 +120,7 @@ class WebClient(AbcClient):
113
120
  return False
114
121
 
115
122
  def _query_user_data(self) -> bool:
116
- if self._server:
123
+ if self._server and self._req is not None:
117
124
  r = self._req.get(self._server + "/user?token=" + self._token, timeout=2)
118
125
  # TODO: possibly a security nightmare (send via json or encrypted via public key?)
119
126
  r.raise_for_status()
@@ -2,12 +2,11 @@
2
2
 
3
3
  import copy
4
4
  import pickle
5
+ from collections.abc import Mapping
5
6
  from datetime import datetime
6
7
  from datetime import timedelta
7
8
  from pathlib import Path
8
9
  from typing import Any
9
- from typing import Dict
10
- from typing import Mapping
11
10
  from typing import Optional
12
11
  from typing import Union
13
12
 
@@ -15,10 +14,11 @@ import yaml
15
14
  from pydantic import validate_call
16
15
  from typing_extensions import Self
17
16
 
18
- from ..data_models.base.timezone import local_now
19
- from ..data_models.base.timezone import local_tz
20
- from ..data_models.base.wrapper import Wrapper
21
- from ..logger import logger
17
+ from shepherd_core.data_models.base.timezone import local_now
18
+ from shepherd_core.data_models.base.timezone import local_tz
19
+ from shepherd_core.data_models.base.wrapper import Wrapper
20
+ from shepherd_core.logger import logger
21
+
22
22
  from .cache_path import cache_user_path
23
23
 
24
24
  # Proposed field-name:
@@ -34,8 +34,8 @@ class Fixture:
34
34
 
35
35
  def __init__(self, model_type: str) -> None:
36
36
  self.model_type: str = model_type.lower()
37
- self.elements_by_name: Dict[str, dict] = {}
38
- self.elements_by_id: Dict[int, dict] = {}
37
+ self.elements_by_name: dict[str, dict] = {}
38
+ self.elements_by_id: dict[int, dict] = {}
39
39
  # Iterator reset
40
40
  self._iter_index: int = 0
41
41
  self._iter_list: list = list(self.elements_by_name.values())
@@ -43,25 +43,26 @@ class Fixture:
43
43
  def insert(self, data: Wrapper) -> None:
44
44
  # ⤷ TODO: could get easier
45
45
  # - when not model_name but class used
46
- # - use doubleref name->id->data (safes RAM)
46
+ # - use doubleref name->id->data (saves RAM)
47
47
  if data.datatype.lower() != self.model_type.lower():
48
48
  return
49
49
  if "name" not in data.parameters:
50
50
  return
51
51
  name = str(data.parameters["name"]).lower()
52
52
  _id = data.parameters["id"]
53
- data = data.parameters
54
- self.elements_by_name[name] = data
55
- self.elements_by_id[_id] = data
53
+ data_model = data.parameters
54
+ self.elements_by_name[name] = data_model
55
+ self.elements_by_id[_id] = data_model
56
56
  # update iterator
57
- self._iter_list: list = list(self.elements_by_name.values())
57
+ self._iter_list = list(self.elements_by_name.values())
58
58
 
59
59
  def __getitem__(self, key: Union[str, int]) -> dict:
60
60
  if isinstance(key, str):
61
61
  key = key.lower()
62
62
  if key in self.elements_by_name:
63
63
  return self.elements_by_name[key]
64
- key = int(key) if key.isdigit() else None
64
+ if key.isdigit():
65
+ key = int(key)
65
66
  if key in self.elements_by_id:
66
67
  return self.elements_by_id[int(key)]
67
68
  msg = f"{self.model_type} '{key}' not found!"
@@ -85,7 +86,9 @@ class Fixture:
85
86
  def refs(self) -> dict:
86
87
  return {_i["id"]: _i["name"] for _i in self.elements_by_id.values()}
87
88
 
88
- def inheritance(self, values: dict, chain: Optional[list] = None) -> (dict, list):
89
+ def inheritance(
90
+ self, values: dict[str, Any], chain: Optional[list[str]] = None
91
+ ) -> tuple[dict[str, Any], list[str]]:
89
92
  if chain is None:
90
93
  chain = []
91
94
  values = copy.copy(values)
@@ -181,7 +184,7 @@ class Fixtures:
181
184
  self.file_path = Path(__file__).parent.parent.resolve() / "data_models"
182
185
  else:
183
186
  self.file_path = file_path
184
- self.components: Dict[str, Fixture] = {}
187
+ self.components: dict[str, Fixture] = {}
185
188
  cache_file = cache_user_path / "fixtures.pickle"
186
189
  sheep_detect = Path("/lib/firmware/am335x-pru0-fw").exists()
187
190
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  import secrets
4
4
  from hashlib import pbkdf2_hmac
5
+ from typing import Annotated
6
+ from typing import Any
5
7
  from typing import Optional
6
8
  from typing import Union
7
9
  from uuid import uuid4
@@ -14,11 +16,10 @@ from pydantic import SecretStr
14
16
  from pydantic import StringConstraints
15
17
  from pydantic import model_validator
16
18
  from pydantic import validate_call
17
- from typing_extensions import Annotated
18
19
 
19
- from ..data_models.base.content import NameStr
20
- from ..data_models.base.content import SafeStr
21
- from ..data_models.base.shepherd import ShpModel
20
+ from shepherd_core.data_models.base.content import NameStr
21
+ from shepherd_core.data_models.base.content import SafeStr
22
+ from shepherd_core.data_models.base.shepherd import ShpModel
22
23
 
23
24
 
24
25
  @validate_call
@@ -63,7 +64,7 @@ class User(ShpModel):
63
64
 
64
65
  @model_validator(mode="before")
65
66
  @classmethod
66
- def query_database(cls, values: dict) -> dict:
67
+ def query_database(cls, values: dict[str, Any]) -> dict[str, Any]:
67
68
  # TODO:
68
69
 
69
70
  # post correction
shepherd_core/version.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Separated string avoids circular imports."""
2
2
 
3
- version: str = "2025.04.1"
3
+ version: str = "2025.04.2"
@@ -79,9 +79,9 @@ class DiodeTarget(TargetABC):
79
79
 
80
80
  def step(self, voltage_uV: int, *, pwr_good: bool) -> float:
81
81
  if pwr_good or not self.ctrl:
82
- V_CC = voltage_uV * 1e-6
83
- V_D = V_CC / 2
84
- I_R = I_D = 0
82
+ V_CC: float = voltage_uV * 1e-6
83
+ V_D: float = V_CC / 2
84
+ I_R = I_D = 0.0
85
85
  # there is no direct formular, but this iteration converges fast
86
86
  for _ in range(10):
87
87
  # low voltages tend to produce log(x<0)=err
@@ -17,9 +17,9 @@ Compromises:
17
17
  import math
18
18
  from typing import Optional
19
19
 
20
- from ..data_models import CalibrationEmulator
21
- from ..data_models.content.virtual_source import LUT_SIZE
22
- from ..data_models.content.virtual_source import ConverterPRUConfig
20
+ from shepherd_core.data_models import CalibrationEmulator
21
+ from shepherd_core.data_models.content.virtual_source import LUT_SIZE
22
+ from shepherd_core.data_models.content.virtual_source import ConverterPRUConfig
23
23
 
24
24
 
25
25
  class PruCalibration: