shepherd-core 2023.8.6__py3-none-any.whl → 2023.8.8__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 (49) hide show
  1. shepherd_core/__init__.py +1 -1
  2. shepherd_core/data_models/__init__.py +3 -1
  3. shepherd_core/data_models/base/cal_measurement.py +17 -14
  4. shepherd_core/data_models/base/calibration.py +41 -8
  5. shepherd_core/data_models/base/content.py +17 -13
  6. shepherd_core/data_models/base/shepherd.py +29 -22
  7. shepherd_core/data_models/base/wrapper.py +5 -4
  8. shepherd_core/data_models/content/energy_environment.py +3 -2
  9. shepherd_core/data_models/content/firmware.py +10 -6
  10. shepherd_core/data_models/content/virtual_harvester.py +42 -39
  11. shepherd_core/data_models/content/virtual_source.py +83 -72
  12. shepherd_core/data_models/doc_virtual_source.py +7 -14
  13. shepherd_core/data_models/experiment/experiment.py +20 -15
  14. shepherd_core/data_models/experiment/observer_features.py +33 -31
  15. shepherd_core/data_models/experiment/target_config.py +24 -18
  16. shepherd_core/data_models/task/__init__.py +13 -5
  17. shepherd_core/data_models/task/emulation.py +35 -23
  18. shepherd_core/data_models/task/firmware_mod.py +14 -13
  19. shepherd_core/data_models/task/harvest.py +28 -13
  20. shepherd_core/data_models/task/observer_tasks.py +17 -7
  21. shepherd_core/data_models/task/programming.py +13 -13
  22. shepherd_core/data_models/task/testbed_tasks.py +16 -6
  23. shepherd_core/data_models/testbed/cape.py +3 -2
  24. shepherd_core/data_models/testbed/gpio.py +18 -15
  25. shepherd_core/data_models/testbed/mcu.py +7 -6
  26. shepherd_core/data_models/testbed/observer.py +23 -19
  27. shepherd_core/data_models/testbed/target.py +15 -14
  28. shepherd_core/data_models/testbed/testbed.py +14 -11
  29. shepherd_core/fw_tools/converter.py +7 -7
  30. shepherd_core/fw_tools/converter_elf.py +2 -2
  31. shepherd_core/fw_tools/patcher.py +7 -6
  32. shepherd_core/fw_tools/validation.py +3 -3
  33. shepherd_core/inventory/__init__.py +16 -8
  34. shepherd_core/inventory/python.py +4 -3
  35. shepherd_core/inventory/system.py +5 -5
  36. shepherd_core/inventory/target.py +4 -4
  37. shepherd_core/reader.py +3 -3
  38. shepherd_core/testbed_client/client.py +6 -4
  39. shepherd_core/testbed_client/user_model.py +14 -10
  40. shepherd_core/writer.py +2 -2
  41. {shepherd_core-2023.8.6.dist-info → shepherd_core-2023.8.8.dist-info}/METADATA +9 -2
  42. {shepherd_core-2023.8.6.dist-info → shepherd_core-2023.8.8.dist-info}/RECORD +49 -49
  43. {shepherd_core-2023.8.6.dist-info → shepherd_core-2023.8.8.dist-info}/WHEEL +1 -1
  44. tests/data_models/example_cal_data.yaml +2 -1
  45. tests/data_models/example_cal_meas.yaml +2 -1
  46. tests/data_models/test_base_models.py +19 -2
  47. tests/inventory/test_inventory.py +1 -1
  48. {shepherd_core-2023.8.6.dist-info → shepherd_core-2023.8.8.dist-info}/top_level.txt +0 -0
  49. {shepherd_core-2023.8.6.dist-info → shepherd_core-2023.8.8.dist-info}/zip-safe +0 -0
@@ -6,10 +6,10 @@ from pathlib import Path
6
6
  from typing import Optional
7
7
  from typing import Union
8
8
 
9
- from pydantic import confloat
10
- from pydantic import conint
11
- from pydantic import root_validator
12
- from pydantic import validate_arguments
9
+ from pydantic import Field
10
+ from pydantic import model_validator
11
+ from pydantic import validate_call
12
+ from typing_extensions import Annotated
13
13
 
14
14
  from ..base.content import IdInt
15
15
  from ..base.shepherd import ShpModel
@@ -41,12 +41,12 @@ class EmulationTask(ShpModel):
41
41
  # General config
42
42
  input_path: Path
43
43
  # ⤷ hdf5 file containing harvesting data
44
- output_path: Optional[Path]
44
+ output_path: Optional[Path] = None
45
45
  # ⤷ dir- or file-path for storing the recorded data:
46
46
  # - providing a directory -> file is named emu_timestamp.h5
47
47
  # - for a complete path the filename is not changed except it exists and
48
48
  # overwrite is disabled -> emu#num.h5
49
- # TODO: should the path be mandatory?
49
+ # TODO: should the output-path be mandatory?
50
50
  force_overwrite: bool = False
51
51
  # ⤷ Overwrite existing file
52
52
  output_compression: Optional[Compression] = Compression.default
@@ -70,7 +70,7 @@ class EmulationTask(ShpModel):
70
70
  pwr_port: TargetPort = TargetPort.A
71
71
  # ⤷ chosen port will be current-monitored (main, connected to virtual Source),
72
72
  # the other port is aux
73
- voltage_aux: Union[confloat(ge=0, le=4.5), str] = 0
73
+ voltage_aux: Union[Annotated[float, Field(ge=0, le=4.5)], str] = 0
74
74
  # ⤷ aux_voltage options:
75
75
  # - 0-4.5 for specific const Voltage (0 V = disabled),
76
76
  # - "buffer" will output intermediate voltage (storage cap of vsource),
@@ -86,35 +86,47 @@ class EmulationTask(ShpModel):
86
86
  gpio_actuation: Optional[GpioActuation] = None
87
87
  sys_logging: Optional[SystemLogging] = SystemLogging()
88
88
 
89
- verbose: conint(ge=0, le=4) = 2
89
+ verbose: Annotated[int, Field(ge=0, le=4)] = 2
90
90
  # ⤷ 0=Errors, 1=Warnings, 2=Info, 3=Debug
91
91
 
92
- @root_validator(pre=False)
93
- def post_validation(cls, values: dict) -> dict:
94
- # TODO: limit paths
95
- has_start = values.get("time_start") is not None
96
- # add local timezone-data
97
- if has_start and values["time_start"].tzinfo is None:
92
+ @model_validator(mode="before")
93
+ @classmethod
94
+ def pre_correction(cls, values: dict) -> dict:
95
+ # convert & add local timezone-data
96
+ has_time = values.get("time_start") is not None
97
+ if has_time and isinstance(values["time_start"], (int, float)):
98
+ values["time_start"] = datetime.fromtimestamp(values["time_start"])
99
+ if has_time and isinstance(values["time_start"], str):
100
+ values["time_start"] = datetime.fromisoformat(values["time_start"])
101
+ if has_time and values["time_start"].tzinfo is None:
98
102
  values["time_start"] = values["time_start"].astimezone()
99
- if has_start and values.get("time_start") < datetime.now().astimezone():
100
- raise ValueError("Start-Time for Emulation can't be in the past.")
101
- if values.get("duration") and values["duration"].total_seconds() < 0:
103
+ return values
104
+
105
+ @model_validator(mode="after")
106
+ def post_validation(self):
107
+ # TODO: limit paths
108
+ has_time = self.time_start is not None
109
+ time_now = datetime.now().astimezone()
110
+ if has_time and self.time_start < time_now:
111
+ raise ValueError(
112
+ "Start-Time for Emulation can't be in the past "
113
+ f"('{self.time_start}' vs '{time_now}'."
114
+ )
115
+ if self.duration and self.duration.total_seconds() < 0:
102
116
  raise ValueError("Task-Duration can't be negative.")
103
- if isinstance(values.get("voltage_aux"), str) and values.get(
104
- "voltage_aux"
105
- ) not in [
117
+ if isinstance(self.voltage_aux, str) and self.voltage_aux not in [
106
118
  "main",
107
119
  "buffer",
108
120
  ]:
109
121
  raise ValueError(
110
122
  "Voltage Aux must be in float (0 - 4.5) or string 'main' / 'mid'."
111
123
  )
112
- if values.get("gpio_actuation") is not None:
124
+ if self.gpio_actuation is not None:
113
125
  raise ValueError("GPIO Actuation not yet implemented!")
114
- return values
126
+ return self
115
127
 
116
128
  @classmethod
117
- @validate_arguments
129
+ @validate_call
118
130
  def from_xp(cls, xp: Experiment, tb: Testbed, tgt_id: IdInt, root_path: Path):
119
131
  obs = tb.get_observer(tgt_id)
120
132
  tgt_cfg = xp.get_target_config(tgt_id)
@@ -3,16 +3,17 @@ from pathlib import Path
3
3
  from typing import Optional
4
4
  from typing import Union
5
5
 
6
- from pydantic import conint
7
- from pydantic import constr
8
- from pydantic import root_validator
9
- from pydantic import validate_arguments
6
+ from pydantic import Field
7
+ from pydantic import model_validator
8
+ from pydantic import validate_call
9
+ from typing_extensions import Annotated
10
10
 
11
11
  from ...logger import logger
12
12
  from ..base.content import IdInt
13
13
  from ..base.shepherd import ShpModel
14
14
  from ..content.firmware import Firmware
15
15
  from ..content.firmware import FirmwareDType
16
+ from ..content.firmware import FirmwareStr
16
17
  from ..experiment.experiment import Experiment
17
18
  from ..testbed import Testbed
18
19
  from ..testbed.target import IdInt16
@@ -22,27 +23,27 @@ from ..testbed.target import MCUPort
22
23
  class FirmwareModTask(ShpModel):
23
24
  """Config for Task that adds the custom ID to the firmware & stores it into a file"""
24
25
 
25
- data: Union[constr(min_length=3, max_length=8_000_000), Path]
26
+ data: Union[FirmwareStr, Path]
26
27
  data_type: FirmwareDType
27
- custom_id: Optional[IdInt16]
28
+ custom_id: Optional[IdInt16] = None
28
29
  firmware_file: Path
29
30
 
30
- verbose: conint(ge=0, le=4) = 2
31
+ verbose: Annotated[int, Field(ge=0, le=4)] = 2
31
32
  # ⤷ 0=Errors, 1=Warnings, 2=Info, 3=Debug
32
33
 
33
- @root_validator(pre=False)
34
- def post_validation(cls, values: dict) -> dict:
35
- if values.get("data_type") in [
34
+ @model_validator(mode="after")
35
+ def post_validation(self):
36
+ if self.data_type in [
36
37
  FirmwareDType.base64_hex,
37
38
  FirmwareDType.path_hex,
38
39
  ]:
39
40
  logger.warning(
40
41
  "Firmware is scheduled to get custom-ID but is not in elf-format"
41
42
  )
42
- return values
43
+ return self
43
44
 
44
45
  @classmethod
45
- @validate_arguments
46
+ @validate_call
46
47
  def from_xp(
47
48
  cls,
48
49
  xp: Experiment,
@@ -70,7 +71,7 @@ class FirmwareModTask(ShpModel):
70
71
  )
71
72
 
72
73
  @classmethod
73
- @validate_arguments
74
+ @validate_call
74
75
  def from_firmware(cls, fw: Firmware, **kwargs):
75
76
  kwargs["data"] = fw.data
76
77
  kwargs["data_type"] = fw.data_type
@@ -3,8 +3,9 @@ from datetime import timedelta
3
3
  from pathlib import Path
4
4
  from typing import Optional
5
5
 
6
- from pydantic import conint
7
- from pydantic import root_validator
6
+ from pydantic import Field
7
+ from pydantic import model_validator
8
+ from typing_extensions import Annotated
8
9
 
9
10
  from ..base.shepherd import ShpModel
10
11
  from ..content.virtual_harvester import VirtualHarvesterConfig
@@ -46,20 +47,34 @@ class HarvestTask(ShpModel):
46
47
  power_tracing: PowerTracing = PowerTracing()
47
48
  sys_logging: Optional[SystemLogging] = SystemLogging()
48
49
 
49
- verbose: conint(ge=0, le=4) = 2
50
+ verbose: Annotated[int, Field(ge=0, le=4)] = 2
50
51
  # ⤷ 0=Errors, 1=Warnings, 2=Info, 3=Debug
51
52
 
52
53
  # TODO: there is an unused DAC-Output patched to the harvesting-port
53
54
 
54
- @root_validator(pre=False)
55
- def post_validation(cls, values: dict) -> dict:
56
- # TODO: limit paths
57
- has_start = values.get("time_start") is not None
58
- if has_start and values["time_start"].tzinfo is None:
59
- # add local timezone-data
55
+ @model_validator(mode="before")
56
+ @classmethod
57
+ def pre_correction(cls, values: dict) -> dict:
58
+ # convert & add local timezone-data, TODO: used twice, refactor
59
+ has_time = values.get("time_start") is not None
60
+ if has_time and isinstance(values["time_start"], (int, float)):
61
+ values["time_start"] = datetime.fromtimestamp(values["time_start"])
62
+ if has_time and isinstance(values["time_start"], str):
63
+ values["time_start"] = datetime.fromisoformat(values["time_start"])
64
+ if has_time and values["time_start"].tzinfo is None:
60
65
  values["time_start"] = values["time_start"].astimezone()
61
- if has_start and values["time_start"] < datetime.now().astimezone():
62
- raise ValueError("Start-Time for Harvest can't be in the past.")
63
- if values.get("duration") and values["duration"].total_seconds() < 0:
64
- raise ValueError("Task-Duration can't be negative.")
65
66
  return values
67
+
68
+ @model_validator(mode="after")
69
+ def post_validation(self):
70
+ # TODO: limit paths
71
+ has_time = self.time_start is not None
72
+ time_now = datetime.now().astimezone()
73
+ if has_time and self.time_start < time_now:
74
+ raise ValueError(
75
+ "Start-Time for Emulation can't be in the past "
76
+ f"('{self.time_start}' vs '{time_now}'."
77
+ )
78
+ if self.duration and self.duration.total_seconds() < 0:
79
+ raise ValueError("Task-Duration can't be negative.")
80
+ return self
@@ -3,7 +3,8 @@ from pathlib import Path
3
3
  from typing import List
4
4
  from typing import Optional
5
5
 
6
- from pydantic import validate_arguments
6
+ from pydantic import computed_field
7
+ from pydantic import validate_call
7
8
 
8
9
  from ..base.content import IdInt
9
10
  from ..base.content import NameStr
@@ -27,13 +28,13 @@ class ObserverTasks(ShpModel):
27
28
  abort_on_error: bool
28
29
 
29
30
  # fw mod, store as hex-file and program
30
- fw1_mod: Optional[FirmwareModTask]
31
- fw2_mod: Optional[FirmwareModTask]
32
- fw1_prog: Optional[ProgrammingTask]
33
- fw2_prog: Optional[ProgrammingTask]
31
+ fw1_mod: Optional[FirmwareModTask] = None
32
+ fw2_mod: Optional[FirmwareModTask] = None
33
+ fw1_prog: Optional[ProgrammingTask] = None
34
+ fw2_prog: Optional[ProgrammingTask] = None
34
35
 
35
36
  # MAIN PROCESS
36
- emulation: Optional[EmulationTask]
37
+ emulation: Optional[EmulationTask] = None
37
38
 
38
39
  # post_copy / cleanup, Todo: could also just intake emuTask
39
40
  # - delete firmwares
@@ -42,7 +43,7 @@ class ObserverTasks(ShpModel):
42
43
  # - zip it
43
44
 
44
45
  @classmethod
45
- @validate_arguments
46
+ @validate_call
46
47
  def from_xp(cls, xp: Experiment, tb: Testbed, tgt_id: IdInt):
47
48
  if not tb.shared_storage:
48
49
  raise ValueError("Implementation currently relies on shared storage!")
@@ -78,3 +79,12 @@ class ObserverTasks(ShpModel):
78
79
  continue
79
80
  tasks.append(task)
80
81
  return tasks
82
+
83
+ @computed_field
84
+ def output_paths(self) -> dict:
85
+ values = {}
86
+ if isinstance(self.emulation, EmulationTask):
87
+ if self.emulation.output_path is None:
88
+ raise ValueError("Emu-Task should have a valid output-path")
89
+ values[self.observer] = self.emulation.output_path
90
+ return values
@@ -1,10 +1,10 @@
1
1
  import copy
2
2
  from pathlib import Path
3
3
 
4
- from pydantic import confloat
5
- from pydantic import conint
6
- from pydantic import root_validator
7
- from pydantic import validate_arguments
4
+ from pydantic import Field
5
+ from pydantic import model_validator
6
+ from pydantic import validate_call
7
+ from typing_extensions import Annotated
8
8
 
9
9
  from ..base.content import IdInt
10
10
  from ..base.content import SafeStr
@@ -24,23 +24,23 @@ class ProgrammingTask(ShpModel):
24
24
  mcu_port: MCUPort = 1
25
25
  mcu_type: SafeStr
26
26
  # ⤷ for later
27
- voltage: confloat(ge=1, lt=5) = 3
28
- datarate: conint(gt=0, le=1_000_000) = 500_000
27
+ voltage: Annotated[float, Field(ge=1, lt=5)] = 3
28
+ datarate: Annotated[int, Field(gt=0, le=1_000_000)] = 500_000
29
29
  protocol: ProgrammerProtocol
30
30
 
31
31
  simulate: bool = False
32
32
 
33
- verbose: conint(ge=0, le=4) = 2
33
+ verbose: Annotated[int, Field(ge=0, le=4)] = 2
34
34
  # ⤷ 0=Errors, 1=Warnings, 2=Info, 3=Debug
35
35
 
36
- @root_validator(pre=False)
37
- def post_validation(cls, values: dict) -> dict:
38
- if values.get("firmware_file").suffix.lower() != ".hex":
39
- ValueError(f"Firmware is not intel-.hex ('{values['firmware_file']}')")
40
- return values
36
+ @model_validator(mode="after")
37
+ def post_validation(self):
38
+ if self.firmware_file.suffix.lower() != ".hex":
39
+ ValueError(f"Firmware is not intel-.hex ('{self.firmware_file}')")
40
+ return self
41
41
 
42
42
  @classmethod
43
- @validate_arguments
43
+ @validate_call
44
44
  def from_xp(
45
45
  cls,
46
46
  xp: Experiment,
@@ -1,8 +1,11 @@
1
+ from typing import List
1
2
  from typing import Optional
2
3
 
3
4
  from pydantic import EmailStr
4
- from pydantic import conlist
5
- from pydantic import validate_arguments
5
+ from pydantic import Field
6
+ from pydantic import computed_field
7
+ from pydantic import validate_call
8
+ from typing_extensions import Annotated
6
9
 
7
10
  from ..base.content import NameStr
8
11
  from ..base.shepherd import ShpModel
@@ -15,13 +18,13 @@ class TestbedTasks(ShpModel):
15
18
  """Collection of tasks for all observers included in experiment"""
16
19
 
17
20
  name: NameStr
18
- observer_tasks: conlist(item_type=ObserverTasks, min_items=1, max_items=64)
21
+ observer_tasks: Annotated[List[ObserverTasks], Field(min_length=1, max_length=64)]
19
22
 
20
23
  # POST PROCESS
21
- email: Optional[EmailStr]
24
+ email: Optional[EmailStr] = None
22
25
 
23
26
  @classmethod
24
- @validate_arguments
27
+ @validate_call
25
28
  def from_xp(cls, xp: Experiment, tb: Optional[Testbed] = None):
26
29
  if tb is None:
27
30
  # TODO: just for testing OK
@@ -32,6 +35,13 @@ class TestbedTasks(ShpModel):
32
35
 
33
36
  def get_observer_tasks(self, observer) -> Optional[ObserverTasks]:
34
37
  for tasks in self.observer_tasks:
35
- if observer in tasks.observer:
38
+ if observer == tasks.observer:
36
39
  return tasks
37
40
  return None
41
+
42
+ @computed_field
43
+ def output_paths(self) -> dict:
44
+ values = {}
45
+ for obt in self.observer_tasks:
46
+ values = {**values, **obt.output_paths}
47
+ return values
@@ -5,7 +5,7 @@ from typing import Optional
5
5
  from typing import Union
6
6
 
7
7
  from pydantic import Field
8
- from pydantic import root_validator
8
+ from pydantic import model_validator
9
9
 
10
10
  from ...testbed_client import tb_client
11
11
  from ..base.content import IdInt
@@ -37,7 +37,8 @@ class Cape(ShpModel, title="Shepherd-Cape"):
37
37
  def __str__(self):
38
38
  return self.name
39
39
 
40
- @root_validator(pre=True)
40
+ @model_validator(mode="before")
41
+ @classmethod
41
42
  def query_database(cls, values: dict) -> dict:
42
43
  values, _ = tb_client.try_completing_model(cls.__name__, values)
43
44
  return values
@@ -1,9 +1,10 @@
1
1
  from enum import Enum
2
2
  from typing import Optional
3
3
 
4
- from pydantic import conint
5
- from pydantic import constr
6
- from pydantic import root_validator
4
+ from pydantic import Field
5
+ from pydantic import StringConstraints
6
+ from pydantic import model_validator
7
+ from typing_extensions import Annotated
7
8
 
8
9
  from ...testbed_client import tb_client
9
10
  from ..base.content import IdInt
@@ -30,31 +31,33 @@ class GPIO(ShpModel, title="GPIO of Observer Node"):
30
31
  comment: Optional[SafeStr] = None
31
32
 
32
33
  direction: Direction = Direction.Input
33
- dir_switch: Optional[constr(max_length=32)]
34
+ dir_switch: Optional[Annotated[str, StringConstraints(max_length=32)]] = None
34
35
 
35
- reg_pru: Optional[constr(max_length=10)] = None
36
- pin_pru: Optional[constr(max_length=10)] = None
37
- reg_sys: Optional[conint(ge=0)] = None
38
- pin_sys: Optional[constr(max_length=10)] = None
36
+ reg_pru: Optional[Annotated[str, StringConstraints(max_length=10)]] = None
37
+ pin_pru: Optional[Annotated[str, StringConstraints(max_length=10)]] = None
38
+ reg_sys: Optional[Annotated[int, Field(ge=0)]] = None
39
+ pin_sys: Optional[Annotated[str, StringConstraints(max_length=10)]] = None
39
40
 
40
41
  def __str__(self):
41
42
  return self.name
42
43
 
43
- @root_validator(pre=True)
44
+ @model_validator(mode="before")
45
+ @classmethod
44
46
  def query_database(cls, values: dict) -> dict:
45
47
  values, _ = tb_client.try_completing_model(cls.__name__, values)
46
48
  return values
47
49
 
48
- @root_validator(pre=False)
49
- def post_validation(cls, values: dict) -> dict:
50
+ @model_validator(mode="after")
51
+ def post_validation(self):
50
52
  # ensure that either pru or sys is used, otherwise instance is considered faulty
51
- no_pru = (values.get("reg_pru") is None) or (values.get("pin_pru") is None)
52
- no_sys = (values.get("reg_sys") is None) or (values.get("pin_sys") is None)
53
+ no_pru = (self.reg_pru is None) or (self.pin_pru is None)
54
+ no_sys = (self.reg_sys is None) or (self.pin_sys is None)
53
55
  if no_pru and no_sys:
54
56
  raise ValueError(
55
- f"GPIO-Instance is faulty -> it needs to use pru or sys, content: {values}"
57
+ "GPIO-Instance is faulty -> "
58
+ f"it needs to use pru or sys, content: {self.model_dump()}"
56
59
  )
57
- return values
60
+ return self
58
61
 
59
62
  def user_controllable(self) -> bool:
60
63
  return ("gpio" in self.name.lower()) and (self.direction in ["IO", "OUT"])
@@ -1,9 +1,9 @@
1
1
  from enum import Enum
2
2
  from typing import Optional
3
3
 
4
- from pydantic import confloat
5
- from pydantic import conint
6
- from pydantic import root_validator
4
+ from pydantic import Field
5
+ from pydantic import model_validator
6
+ from typing_extensions import Annotated
7
7
 
8
8
  from ...testbed_client import tb_client
9
9
  from ..base.content import IdInt
@@ -34,8 +34,8 @@ class MCU(ShpModel, title="Microcontroller of the Target Node"):
34
34
  platform: NameStr
35
35
  core: NameStr
36
36
  prog_protocol: ProgrammerProtocol
37
- prog_voltage: confloat(ge=1, le=5) = 3
38
- prog_datarate: conint(gt=0, le=1_000_000) = 500_000
37
+ prog_voltage: Annotated[float, Field(ge=1, le=5)] = 3
38
+ prog_datarate: Annotated[int, Field(gt=0, le=1_000_000)] = 500_000
39
39
 
40
40
  fw_name_default: str
41
41
  # ⤷ can't be FW-Object (circular import)
@@ -43,7 +43,8 @@ class MCU(ShpModel, title="Microcontroller of the Target Node"):
43
43
  def __str__(self):
44
44
  return self.name
45
45
 
46
- @root_validator(pre=True)
46
+ @model_validator(mode="before")
47
+ @classmethod
47
48
  def query_database(cls, values: dict) -> dict:
48
49
  values, _ = tb_client.try_completing_model(cls.__name__, values)
49
50
  return values
@@ -3,9 +3,9 @@ from typing import Optional
3
3
 
4
4
  from pydantic import Field
5
5
  from pydantic import IPvAnyAddress
6
- from pydantic import confloat
7
- from pydantic import constr
8
- from pydantic import root_validator
6
+ from pydantic import StringConstraints
7
+ from pydantic import model_validator
8
+ from typing_extensions import Annotated
9
9
 
10
10
  from ...testbed_client import tb_client
11
11
  from ..base.content import IdInt
@@ -16,7 +16,12 @@ from .cape import Cape
16
16
  from .cape import TargetPort
17
17
  from .target import Target
18
18
 
19
- MACStr = constr(max_length=17, regex=r"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$")
19
+ MACStr = Annotated[
20
+ str,
21
+ StringConstraints(
22
+ max_length=17, pattern=r"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$"
23
+ ),
24
+ ]
20
25
 
21
26
 
22
27
  class Observer(ShpModel, title="Shepherd-Sheep"):
@@ -33,36 +38,35 @@ class Observer(ShpModel, title="Shepherd-Sheep"):
33
38
  room: NameStr
34
39
  eth_port: NameStr
35
40
 
36
- latitude: confloat(ge=-90, le=90) = 51.026573 # cfaed
37
- longitude: confloat(ge=-180, le=180) = 13.723291
41
+ latitude: Annotated[float, Field(ge=-90, le=90)] = 51.026573
42
+ longitude: Annotated[float, Field(ge=-180, le=180)] = 13.723291
43
+ # ⤷ cfaed-floor
38
44
 
39
- cape: Optional[Cape]
40
- target_a: Optional[Target]
45
+ cape: Optional[Cape] = None
46
+ target_a: Optional[Target] = None
41
47
  target_b: Optional[Target] = None
42
48
 
43
49
  created: datetime = Field(default_factory=datetime.now)
44
- alive_last: Optional[datetime]
50
+ alive_last: Optional[datetime] = None
45
51
 
46
52
  def __str__(self):
47
53
  return self.name
48
54
 
49
- @root_validator(pre=True)
55
+ @model_validator(mode="before")
56
+ @classmethod
50
57
  def query_database(cls, values: dict) -> dict:
51
58
  values, _ = tb_client.try_completing_model(cls.__name__, values)
52
59
  return values
53
60
 
54
- @root_validator(pre=False)
55
- def post_validation(cls, values: dict) -> dict:
56
- has_cape = values.get("cape") is not None
57
- has_target = (values.get("target_a") is not None) or (
58
- values.get("target_b") is not None
59
- )
61
+ @model_validator(mode="after")
62
+ def post_validation(self):
63
+ has_cape = self.cape is not None
64
+ has_target = (self.target_a is not None) or (self.target_b is not None)
60
65
  if not has_cape and has_target:
61
66
  raise ValueError(
62
- f"Observer '{values.get('name')}' is faulty "
63
- f"-> has targets but no cape"
67
+ f"Observer '{self.name}' is faulty " f"-> has targets but no cape"
64
68
  )
65
- return values
69
+ return self
66
70
 
67
71
  def get_target_port(self, target_id: int) -> TargetPort:
68
72
  if self.target_a is not None and target_id == self.target_a.id:
@@ -3,8 +3,8 @@ from typing import Optional
3
3
  from typing import Union
4
4
 
5
5
  from pydantic import Field
6
- from pydantic import conint
7
- from pydantic import root_validator
6
+ from pydantic import model_validator
7
+ from typing_extensions import Annotated
8
8
 
9
9
  from ...testbed_client import tb_client
10
10
  from ..base.content import IdInt
@@ -13,9 +13,9 @@ from ..base.content import SafeStr
13
13
  from ..base.shepherd import ShpModel
14
14
  from .mcu import MCU
15
15
 
16
- IdInt16 = conint(ge=0, lt=2**16)
16
+ IdInt16 = Annotated[int, Field(ge=0, lt=2**16)]
17
17
 
18
- MCUPort = conint(ge=1, le=2)
18
+ MCUPort = Annotated[int, Field(ge=1, le=2)]
19
19
 
20
20
 
21
21
  class Target(ShpModel, title="Target Node (DuT)"):
@@ -30,7 +30,7 @@ class Target(ShpModel, title="Target Node (DuT)"):
30
30
 
31
31
  created: datetime = Field(default_factory=datetime.now)
32
32
 
33
- fw_id: Optional[IdInt16]
33
+ fw_id: Optional[IdInt16] = None
34
34
  mcu1: Union[MCU, NameStr]
35
35
  mcu2: Union[MCU, NameStr, None] = None
36
36
  #
@@ -40,18 +40,19 @@ class Target(ShpModel, title="Target Node (DuT)"):
40
40
  def __str__(self):
41
41
  return self.name
42
42
 
43
- @root_validator(pre=True)
43
+ @model_validator(mode="before")
44
+ @classmethod
44
45
  def query_database(cls, values: dict) -> dict:
45
46
  values, _ = tb_client.try_completing_model(cls.__name__, values)
46
- return values
47
47
 
48
- @root_validator(pre=False)
49
- def post_correction(cls, values: dict) -> dict:
50
- if isinstance(values.get("mcu1"), str):
51
- values["mcu1"] = MCU(name=values["mcu1"])
52
- # ⤷ this will raise if default is faulty
53
- if isinstance(values.get("mcu2"), str):
54
- values["mcu2"] = MCU(name=values["mcu2"])
48
+ # post correction
49
+ for _mcu in ["mcu1", "mcu2"]:
50
+ if isinstance(values.get(_mcu), str):
51
+ values[_mcu] = MCU(name=values[_mcu])
52
+ # ⤷ this will raise if default is faulty
53
+ elif isinstance(values.get(_mcu), dict):
54
+ values[_mcu] = MCU(**values[_mcu])
55
55
  if values.get("fw_id") is None:
56
56
  values["fw_id"] = values.get("id") % 2**16
57
+
57
58
  return values