shepherd-core 2024.7.2__py3-none-any.whl → 2024.7.4__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.
- shepherd_core/__init__.py +4 -5
- shepherd_core/calibration_hw_def.py +15 -12
- shepherd_core/data_models/base/calibration.py +4 -3
- shepherd_core/data_models/base/wrapper.py +4 -0
- shepherd_core/data_models/content/energy_environment.py +1 -1
- shepherd_core/data_models/content/firmware.py +1 -0
- shepherd_core/data_models/experiment/experiment.py +8 -7
- shepherd_core/data_models/task/testbed_tasks.py +3 -2
- shepherd_core/data_models/testbed/testbed.py +9 -0
- shepherd_core/testbed_client/__init__.py +3 -1
- shepherd_core/testbed_client/client_abc_fix.py +126 -0
- shepherd_core/testbed_client/client_web.py +157 -0
- shepherd_core/testbed_client/fixtures.py +14 -8
- shepherd_core/version.py +3 -0
- {shepherd_core-2024.7.2.dist-info → shepherd_core-2024.7.4.dist-info}/METADATA +9 -7
- {shepherd_core-2024.7.2.dist-info → shepherd_core-2024.7.4.dist-info}/RECORD +19 -17
- {shepherd_core-2024.7.2.dist-info → shepherd_core-2024.7.4.dist-info}/WHEEL +1 -1
- shepherd_core/testbed_client/client.py +0 -161
- {shepherd_core-2024.7.2.dist-info → shepherd_core-2024.7.4.dist-info}/top_level.txt +0 -0
- {shepherd_core-2024.7.2.dist-info → shepherd_core-2024.7.4.dist-info}/zip-safe +0 -0
shepherd_core/__init__.py
CHANGED
|
@@ -19,11 +19,11 @@ from .logger import get_verbose_level
|
|
|
19
19
|
from .logger import increase_verbose_level
|
|
20
20
|
from .logger import logger
|
|
21
21
|
from .reader import Reader
|
|
22
|
-
from .testbed_client.
|
|
23
|
-
from .
|
|
22
|
+
from .testbed_client.client_web import WebClient
|
|
23
|
+
from .version import version
|
|
24
24
|
from .writer import Writer
|
|
25
25
|
|
|
26
|
-
__version__ =
|
|
26
|
+
__version__ = version
|
|
27
27
|
|
|
28
28
|
__all__ = [
|
|
29
29
|
"Reader",
|
|
@@ -40,7 +40,6 @@ __all__ = [
|
|
|
40
40
|
"local_now",
|
|
41
41
|
"Calc_t",
|
|
42
42
|
"Compression",
|
|
43
|
-
"
|
|
44
|
-
"tb_client", # using this (instead of the Class) is the cleaner, but less pythonic way
|
|
43
|
+
"WebClient",
|
|
45
44
|
"Inventory",
|
|
46
45
|
]
|
|
@@ -32,45 +32,48 @@ RAW_MAX_ADC = 2**M_ADC - 1
|
|
|
32
32
|
RAW_MAX_DAC = 2**M_DAC - 1
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
def adc_current_to_raw(current: float) -> int:
|
|
35
|
+
def adc_current_to_raw(current: float, *, limited: bool = True) -> int:
|
|
36
36
|
"""Convert back a current [A] to raw ADC value."""
|
|
37
37
|
# voltage on input of adc
|
|
38
38
|
val_adc = G_INST_AMP * R_SHT * current
|
|
39
39
|
# digital value according to ADC gain
|
|
40
40
|
val_raw = int(val_adc * (2**M_ADC) / (G_ADC_I * V_REF_ADC))
|
|
41
|
-
return min(max(val_raw, 0), 2**M_ADC - 1)
|
|
41
|
+
return min(max(val_raw, 0), 2**M_ADC - 1) if limited else val_raw
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
def adc_raw_to_current(value: int) -> float:
|
|
44
|
+
def adc_raw_to_current(value: int, *, limited: bool = True) -> float:
|
|
45
45
|
"""Convert a raw ADC value to a current [A]."""
|
|
46
|
-
|
|
46
|
+
if limited:
|
|
47
|
+
value = min(max(value, 0), 2**M_ADC - 1)
|
|
47
48
|
# voltage on input of adc
|
|
48
49
|
val_adc = float(value) * (G_ADC_I * V_REF_ADC) / (2**M_ADC)
|
|
49
50
|
# current according to adc value
|
|
50
51
|
return val_adc / (R_SHT * G_INST_AMP)
|
|
51
52
|
|
|
52
53
|
|
|
53
|
-
def adc_voltage_to_raw(voltage: float) -> int:
|
|
54
|
+
def adc_voltage_to_raw(voltage: float, *, limited: bool = True) -> int:
|
|
54
55
|
"""Convert back a voltage [V] to raw ADC value."""
|
|
55
56
|
# digital value according to ADC gain
|
|
56
57
|
val_raw = int(voltage * (2**M_ADC) / (G_ADC_V * V_REF_ADC))
|
|
57
|
-
return min(max(val_raw, 0), 2**M_ADC - 1)
|
|
58
|
+
return min(max(val_raw, 0), 2**M_ADC - 1) if limited else val_raw
|
|
58
59
|
|
|
59
60
|
|
|
60
|
-
def adc_raw_to_voltage(value: int) -> float:
|
|
61
|
+
def adc_raw_to_voltage(value: int, *, limited: bool = True) -> float:
|
|
61
62
|
"""Convert a raw ADC value to a voltage [V]."""
|
|
62
|
-
|
|
63
|
+
if limited:
|
|
64
|
+
value = min(max(value, 0), 2**M_ADC - 1)
|
|
63
65
|
# voltage according to ADC value
|
|
64
66
|
return float(value) * (G_ADC_V * V_REF_ADC) / (2**M_ADC)
|
|
65
67
|
|
|
66
68
|
|
|
67
|
-
def dac_raw_to_voltage(value: int) -> float:
|
|
69
|
+
def dac_raw_to_voltage(value: int, *, limited: bool = True) -> float:
|
|
68
70
|
"""Convert back a raw DAC value to a voltage [V]."""
|
|
69
|
-
|
|
71
|
+
if limited:
|
|
72
|
+
value = min(max(value, 0), 2**M_DAC - 1)
|
|
70
73
|
return float(value) * (V_REF_DAC * G_DAC) / (2**M_DAC)
|
|
71
74
|
|
|
72
75
|
|
|
73
|
-
def dac_voltage_to_raw(voltage: float) -> int:
|
|
76
|
+
def dac_voltage_to_raw(voltage: float, *, limited: bool = True) -> int:
|
|
74
77
|
"""Convert a voltage [V] to raw DAC value."""
|
|
75
78
|
val_raw = int(voltage * (2**M_DAC) / (V_REF_DAC * G_DAC))
|
|
76
|
-
return min(max(val_raw, 0), 2**M_DAC - 1)
|
|
79
|
+
return min(max(val_raw, 0), 2**M_DAC - 1) if limited else val_raw
|
|
@@ -51,6 +51,7 @@ class CalibrationPair(ShpModel):
|
|
|
51
51
|
|
|
52
52
|
gain: PositiveFloat
|
|
53
53
|
offset: float = 0
|
|
54
|
+
# TODO: add unit
|
|
54
55
|
|
|
55
56
|
def raw_to_si(self, values_raw: Calc_t, *, allow_negative: bool = True) -> Calc_t:
|
|
56
57
|
"""Convert between physical units and raw unsigned integers."""
|
|
@@ -79,8 +80,8 @@ class CalibrationPair(ShpModel):
|
|
|
79
80
|
@classmethod
|
|
80
81
|
def from_fn(cls, fn: Callable) -> Self:
|
|
81
82
|
"""Probe linear function to determine scaling values."""
|
|
82
|
-
offset = fn(0)
|
|
83
|
-
gain_inv = fn(1.0) - offset
|
|
83
|
+
offset = fn(0, limited=False)
|
|
84
|
+
gain_inv = fn(1.0, limited=False) - offset
|
|
84
85
|
return cls(
|
|
85
86
|
gain=1.0 / float(gain_inv),
|
|
86
87
|
offset=-float(offset) / gain_inv,
|
|
@@ -274,7 +275,7 @@ class CalibrationSeries(ShpModel):
|
|
|
274
275
|
emu_port_a: bool = True,
|
|
275
276
|
) -> Self:
|
|
276
277
|
if isinstance(cal, CalibrationHarvester):
|
|
277
|
-
return cls(voltage=cal.adc_V_Sense, current=cal.
|
|
278
|
+
return cls(voltage=cal.adc_V_Sense, current=cal.adc_C_Hrv)
|
|
278
279
|
if emu_port_a:
|
|
279
280
|
return cls(voltage=cal.dac_V_A, current=cal.adc_C_A)
|
|
280
281
|
return cls(voltage=cal.dac_V_B, current=cal.adc_C_B)
|
|
@@ -7,6 +7,8 @@ from pydantic import BaseModel
|
|
|
7
7
|
from pydantic import StringConstraints
|
|
8
8
|
from typing_extensions import Annotated
|
|
9
9
|
|
|
10
|
+
from ...version import version
|
|
11
|
+
|
|
10
12
|
SafeStrClone = Annotated[str, StringConstraints(pattern=r"^[ -~]+$")]
|
|
11
13
|
# ⤷ copy avoids circular import
|
|
12
14
|
|
|
@@ -19,5 +21,7 @@ class Wrapper(BaseModel):
|
|
|
19
21
|
comment: Optional[SafeStrClone] = None
|
|
20
22
|
created: Optional[datetime] = None
|
|
21
23
|
# ⤷ Optional metadata
|
|
24
|
+
lib_ver: Optional[str] = version
|
|
25
|
+
# ⤷ for debug-purposes and later comp-checks
|
|
22
26
|
parameters: dict
|
|
23
27
|
# ⤷ ShpModel
|
|
@@ -35,7 +35,7 @@ class EnergyEnvironment(ContentModel):
|
|
|
35
35
|
|
|
36
36
|
# TODO: scale up/down voltage/current
|
|
37
37
|
|
|
38
|
-
# additional descriptive metadata
|
|
38
|
+
# additional descriptive metadata, TODO: these are very solar-centered -> generalize
|
|
39
39
|
light_source: Optional[str] = None
|
|
40
40
|
weather_conditions: Optional[str] = None
|
|
41
41
|
indoor: Optional[bool] = None
|
|
@@ -13,6 +13,7 @@ from pydantic import model_validator
|
|
|
13
13
|
from typing_extensions import Annotated
|
|
14
14
|
from typing_extensions import Self
|
|
15
15
|
|
|
16
|
+
from ...version import version
|
|
16
17
|
from ..base.content import IdInt
|
|
17
18
|
from ..base.content import NameStr
|
|
18
19
|
from ..base.content import SafeStr
|
|
@@ -54,18 +55,20 @@ class Experiment(ShpModel, title="Config of an Experiment"):
|
|
|
54
55
|
# targets
|
|
55
56
|
target_configs: Annotated[List[TargetConfig], Field(min_length=1, max_length=128)]
|
|
56
57
|
|
|
57
|
-
#
|
|
58
|
+
# for debug-purposes and later comp-checks
|
|
59
|
+
lib_ver: Optional[str] = version
|
|
58
60
|
|
|
59
61
|
@model_validator(mode="after")
|
|
60
62
|
def post_validation(self) -> Self:
|
|
61
|
-
|
|
62
|
-
self.
|
|
63
|
+
testbed = Testbed() # this will query the first (and only) entry of client
|
|
64
|
+
self._validate_targets(self.target_configs)
|
|
65
|
+
self._validate_observers(self.target_configs, testbed)
|
|
63
66
|
if self.duration and self.duration.total_seconds() < 0:
|
|
64
67
|
raise ValueError("Duration of experiment can't be negative.")
|
|
65
68
|
return self
|
|
66
69
|
|
|
67
70
|
@staticmethod
|
|
68
|
-
def
|
|
71
|
+
def _validate_targets(configs: List[TargetConfig]) -> None:
|
|
69
72
|
target_ids = []
|
|
70
73
|
custom_ids = []
|
|
71
74
|
for _config in configs:
|
|
@@ -83,10 +86,8 @@ class Experiment(ShpModel, title="Config of an Experiment"):
|
|
|
83
86
|
raise ValueError("Custom Target-ID are faulty (some form of id-collisions)!")
|
|
84
87
|
|
|
85
88
|
@staticmethod
|
|
86
|
-
def
|
|
89
|
+
def _validate_observers(configs: List[TargetConfig], testbed: Testbed) -> None:
|
|
87
90
|
target_ids = [_id for _config in configs for _id in _config.target_IDs]
|
|
88
|
-
|
|
89
|
-
testbed = Testbed(name="shepherd_tud_nes")
|
|
90
91
|
obs_ids = [testbed.get_observer(_id).id for _id in target_ids]
|
|
91
92
|
if len(target_ids) > len(set(obs_ids)):
|
|
92
93
|
raise ValueError(
|
|
@@ -32,8 +32,9 @@ class TestbedTasks(ShpModel):
|
|
|
32
32
|
@validate_call
|
|
33
33
|
def from_xp(cls, xp: Experiment, tb: Optional[Testbed] = None) -> Self:
|
|
34
34
|
if tb is None:
|
|
35
|
-
# TODO:
|
|
36
|
-
tb = Testbed(
|
|
35
|
+
# TODO: is tb-argument really needed? prob. not
|
|
36
|
+
tb = Testbed() # this will query the first (and only) entry of client
|
|
37
|
+
|
|
37
38
|
tgt_ids = xp.get_target_ids()
|
|
38
39
|
obs_tasks = [ObserverTasks.from_xp(xp, tb, _id) for _id in tgt_ids]
|
|
39
40
|
return cls(
|
|
@@ -11,6 +11,7 @@ from pydantic import model_validator
|
|
|
11
11
|
from typing_extensions import Annotated
|
|
12
12
|
from typing_extensions import Self
|
|
13
13
|
|
|
14
|
+
from ... import logger
|
|
14
15
|
from ...testbed_client import tb_client
|
|
15
16
|
from ..base.content import IdInt
|
|
16
17
|
from ..base.content import NameStr
|
|
@@ -42,6 +43,14 @@ class Testbed(ShpModel):
|
|
|
42
43
|
@model_validator(mode="before")
|
|
43
44
|
@classmethod
|
|
44
45
|
def query_database(cls, values: dict) -> dict:
|
|
46
|
+
# allow instantiating an empty Testbed
|
|
47
|
+
# -> query the first (and only) entry of client
|
|
48
|
+
if len(values) == 0:
|
|
49
|
+
ids = tb_client.query_ids(cls.__name__)
|
|
50
|
+
if len(ids) > 1:
|
|
51
|
+
logger.warning("More than one testbed defined?!?")
|
|
52
|
+
values = {"id": ids[0]}
|
|
53
|
+
|
|
45
54
|
values, _ = tb_client.try_completing_model(cls.__name__, values)
|
|
46
55
|
return values
|
|
47
56
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"""Client to access a testbed-instance for controlling experiments."""
|
|
2
2
|
|
|
3
|
-
from .
|
|
3
|
+
from .client_abc_fix import tb_client
|
|
4
|
+
from .client_web import WebClient
|
|
4
5
|
from .user_model import User
|
|
5
6
|
|
|
6
7
|
__all__ = [
|
|
7
8
|
"tb_client",
|
|
9
|
+
"WebClient",
|
|
8
10
|
"User",
|
|
9
11
|
]
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""AbstractBase-Class & Client-Class to access the file based fixtures.
|
|
2
|
+
|
|
3
|
+
Fixtures == OffLineDemoInstances
|
|
4
|
+
offline: core - fixtClient
|
|
5
|
+
webDev: core - webClient <-> webSrv - fixtClient
|
|
6
|
+
webUser: core - webClient <-> webSrv - DbClient
|
|
7
|
+
webInfra: core - webClient+ <-> webSrv - DbClient
|
|
8
|
+
|
|
9
|
+
Users, Sheep and ServerApps should have access to the same DB via WebClient
|
|
10
|
+
|
|
11
|
+
Note: ABC and FixClient can't be in separate files when tb_client should
|
|
12
|
+
default to FixClient (circular import)
|
|
13
|
+
|
|
14
|
+
TODO: Comfort functions missing
|
|
15
|
+
- fixtures to DB, and vice versa
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from abc import ABC
|
|
19
|
+
from abc import abstractmethod
|
|
20
|
+
from typing import List
|
|
21
|
+
from typing import Optional
|
|
22
|
+
|
|
23
|
+
from ..data_models.base.shepherd import ShpModel
|
|
24
|
+
from ..data_models.base.wrapper import Wrapper
|
|
25
|
+
from .fixtures import Fixtures
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AbcClient(ABC):
|
|
29
|
+
"""AbstractBase-Class to access a testbed instance."""
|
|
30
|
+
|
|
31
|
+
def __init__(self) -> None:
|
|
32
|
+
global tb_client # noqa: PLW0603
|
|
33
|
+
tb_client = self
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def insert(self, data: ShpModel) -> bool:
|
|
37
|
+
"""Insert (and probably replace) entry.
|
|
38
|
+
|
|
39
|
+
TODO: fixtures get replaced, but is that wanted for web?
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def query_ids(self, model_type: str) -> List[int]:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def query_names(self, model_type: str) -> List[str]:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def query_item(
|
|
52
|
+
self, model_type: str, uid: Optional[int] = None, name: Optional[str] = None
|
|
53
|
+
) -> dict:
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def try_inheritance(self, model_type: str, values: dict) -> (dict, list):
|
|
58
|
+
# TODO: maybe internal? yes
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
def try_completing_model(self, model_type: str, values: dict) -> (dict, list):
|
|
62
|
+
"""Init by name/id, for none existing instances raise Exception.
|
|
63
|
+
|
|
64
|
+
This is the main entry-point for querying a model (used be the core-lib).
|
|
65
|
+
"""
|
|
66
|
+
if len(values) == 1 and next(iter(values.keys())) in {"id", "name"}:
|
|
67
|
+
try:
|
|
68
|
+
values = self.query_item(model_type, name=values.get("name"), uid=values.get("id"))
|
|
69
|
+
except ValueError as err:
|
|
70
|
+
raise ValueError(
|
|
71
|
+
"Query %s by name / ID failed - %s is unknown!", model_type, values
|
|
72
|
+
) from err
|
|
73
|
+
return self.try_inheritance(model_type, values)
|
|
74
|
+
|
|
75
|
+
@abstractmethod
|
|
76
|
+
def fill_in_user_data(self, values: dict) -> dict:
|
|
77
|
+
# TODO: is it really helpful and needed?
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class FixturesClient(AbcClient):
|
|
82
|
+
"""Client-Class to access the file based fixtures."""
|
|
83
|
+
|
|
84
|
+
def __init__(self) -> None:
|
|
85
|
+
super().__init__()
|
|
86
|
+
self._fixtures: Optional[Fixtures] = Fixtures()
|
|
87
|
+
|
|
88
|
+
def insert(self, data: ShpModel) -> bool:
|
|
89
|
+
wrap = Wrapper(
|
|
90
|
+
datatype=type(data).__name__,
|
|
91
|
+
parameters=data.model_dump(),
|
|
92
|
+
)
|
|
93
|
+
self._fixtures.insert_model(wrap)
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
def query_ids(self, model_type: str) -> List[int]:
|
|
97
|
+
return list(self._fixtures[model_type].elements_by_id.keys())
|
|
98
|
+
|
|
99
|
+
def query_names(self, model_type: str) -> List[str]:
|
|
100
|
+
return list(self._fixtures[model_type].elements_by_name.keys())
|
|
101
|
+
|
|
102
|
+
def query_item(
|
|
103
|
+
self, model_type: str, uid: Optional[int] = None, name: Optional[str] = None
|
|
104
|
+
) -> dict:
|
|
105
|
+
if uid is not None:
|
|
106
|
+
return self._fixtures[model_type].query_id(uid)
|
|
107
|
+
if name is not None:
|
|
108
|
+
return self._fixtures[model_type].query_name(name)
|
|
109
|
+
raise ValueError("Query needs either uid or name of object")
|
|
110
|
+
|
|
111
|
+
def try_inheritance(self, model_type: str, values: dict) -> (dict, list):
|
|
112
|
+
return self._fixtures[model_type].inheritance(values)
|
|
113
|
+
|
|
114
|
+
def fill_in_user_data(self, values: dict) -> dict:
|
|
115
|
+
"""Add fake user-data when offline-client is used.
|
|
116
|
+
|
|
117
|
+
Hotfix until WebClient is working.
|
|
118
|
+
"""
|
|
119
|
+
if values.get("owner") is None:
|
|
120
|
+
values["owner"] = "unknown"
|
|
121
|
+
if values.get("group") is None:
|
|
122
|
+
values["group"] = "unknown"
|
|
123
|
+
return values
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
tb_client: AbcClient = FixturesClient()
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Client-Class to access a testbed instance over the web."""
|
|
2
|
+
|
|
3
|
+
from importlib import import_module
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from typing import Union
|
|
8
|
+
|
|
9
|
+
from pydantic import validate_call
|
|
10
|
+
|
|
11
|
+
from ..commons import testbed_server_default
|
|
12
|
+
from ..data_models.base.shepherd import ShpModel
|
|
13
|
+
from ..data_models.base.wrapper import Wrapper
|
|
14
|
+
from .client_abc_fix import AbcClient
|
|
15
|
+
from .user_model import User
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class WebClient(AbcClient):
|
|
19
|
+
"""Client-Class to access a testbed instance over the web.
|
|
20
|
+
|
|
21
|
+
For online-queries the lib can be connected to the testbed-server.
|
|
22
|
+
NOTE: there are 3 states:
|
|
23
|
+
- unconnected -> demo-fixtures are queried (locally)
|
|
24
|
+
- connected -> publicly available data is queried online
|
|
25
|
+
- logged in with valid token -> also private data is queried online
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
testbed_server_default = "https://shepherd.cfaed.tu-dresden.de:8000/testbed"
|
|
29
|
+
|
|
30
|
+
def __init__(self, server: Optional[str] = None, token: Union[str, Path, None] = None) -> None:
|
|
31
|
+
"""Connect to Testbed-Server with optional token and server-address.
|
|
32
|
+
|
|
33
|
+
server: optional address to shepherd-server-endpoint
|
|
34
|
+
token: your account validation. if omitted, only public data is available
|
|
35
|
+
"""
|
|
36
|
+
super().__init__()
|
|
37
|
+
if not hasattr(self, "_token"):
|
|
38
|
+
# add default values
|
|
39
|
+
self._token: str = "basic_public_access"
|
|
40
|
+
self._server: str = testbed_server_default
|
|
41
|
+
self._user: Optional[User] = None
|
|
42
|
+
self._key: Optional[str] = None
|
|
43
|
+
self._connected: bool = False
|
|
44
|
+
self._req = None
|
|
45
|
+
|
|
46
|
+
if not self._connected:
|
|
47
|
+
self._connect(server, token)
|
|
48
|
+
|
|
49
|
+
# ABC Functions below
|
|
50
|
+
|
|
51
|
+
def insert(self, data: ShpModel) -> bool:
|
|
52
|
+
wrap = Wrapper(
|
|
53
|
+
datatype=type(data).__name__,
|
|
54
|
+
parameters=data.model_dump(),
|
|
55
|
+
)
|
|
56
|
+
r = self._req.post(self._server + "/add", data=wrap.model_dump_json(), timeout=2)
|
|
57
|
+
r.raise_for_status()
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
def query_ids(self, model_type: str) -> List[int]:
|
|
61
|
+
raise NotImplementedError("TODO")
|
|
62
|
+
|
|
63
|
+
def query_names(self, model_type: str) -> List[str]:
|
|
64
|
+
raise NotImplementedError("TODO")
|
|
65
|
+
|
|
66
|
+
def query_item(
|
|
67
|
+
self, model_type: str, uid: Optional[int] = None, name: Optional[str] = None
|
|
68
|
+
) -> dict:
|
|
69
|
+
raise NotImplementedError("TODO")
|
|
70
|
+
|
|
71
|
+
def try_inheritance(self, model_type: str, values: dict) -> (dict, list):
|
|
72
|
+
raise NotImplementedError("TODO")
|
|
73
|
+
|
|
74
|
+
def fill_in_user_data(self, values: dict) -> dict:
|
|
75
|
+
if values.get("owner") is None:
|
|
76
|
+
values["owner"] = self._user.name
|
|
77
|
+
if values.get("group") is None:
|
|
78
|
+
values["group"] = self._user.group
|
|
79
|
+
return values
|
|
80
|
+
|
|
81
|
+
# Below are extra FNs not in ABC
|
|
82
|
+
|
|
83
|
+
@validate_call
|
|
84
|
+
def _connect(self, server: Optional[str] = None, token: Union[str, Path, None] = None) -> bool:
|
|
85
|
+
"""Establish connection to testbed-server.
|
|
86
|
+
|
|
87
|
+
TODO: totally not finished
|
|
88
|
+
"""
|
|
89
|
+
if isinstance(token, Path):
|
|
90
|
+
if not token.exists():
|
|
91
|
+
raise FileNotFoundError("Token-Path does not exist")
|
|
92
|
+
with token.resolve().open() as file:
|
|
93
|
+
self._token = file.read()
|
|
94
|
+
elif isinstance(token, str):
|
|
95
|
+
self._token = self._token
|
|
96
|
+
|
|
97
|
+
if isinstance(server, str):
|
|
98
|
+
self._server = server.lower()
|
|
99
|
+
|
|
100
|
+
self._req = import_module("requests") # here due to slow startup
|
|
101
|
+
|
|
102
|
+
# extended connection-test:
|
|
103
|
+
self._query_session_key()
|
|
104
|
+
self._connected = True
|
|
105
|
+
return self._query_user_data()
|
|
106
|
+
|
|
107
|
+
def _query_session_key(self) -> bool:
|
|
108
|
+
if self._server:
|
|
109
|
+
r = self._req.get(self._server + "/session_key", timeout=2)
|
|
110
|
+
r.raise_for_status()
|
|
111
|
+
self._key = r.json()["value"] # TODO: not finished
|
|
112
|
+
return True
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
def _query_user_data(self) -> bool:
|
|
116
|
+
if self._server:
|
|
117
|
+
r = self._req.get(self._server + "/user?token=" + self._token, timeout=2)
|
|
118
|
+
# TODO: possibly a security nightmare (send via json or encrypted via public key?)
|
|
119
|
+
r.raise_for_status()
|
|
120
|
+
self._user = User(**r.json())
|
|
121
|
+
return True
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
def submit_experiment(self, xp: ShpModel) -> str:
|
|
125
|
+
"""Transmit XP to server to validate its feasibility.
|
|
126
|
+
|
|
127
|
+
- Experiment will be added to DB (if not present)
|
|
128
|
+
- if the same experiment is resubmitted it will just return the ID of that XP
|
|
129
|
+
- Experiment will be validated by converting it into a task-set (additional validation)
|
|
130
|
+
- optional: the scheduler should validate there are no time-collisions
|
|
131
|
+
|
|
132
|
+
Will return an ID if valid, otherwise an empty string.
|
|
133
|
+
TODO: maybe its better to throw specific errors if validation fails
|
|
134
|
+
TODO: is it better to include these experiment-related FNs in Xp-Class?
|
|
135
|
+
TODO: Experiment-typehint for argument triggers circular import
|
|
136
|
+
"""
|
|
137
|
+
raise NotImplementedError("TODO")
|
|
138
|
+
|
|
139
|
+
def schedule_experiment(self, id_xp: str) -> bool:
|
|
140
|
+
"""Enqueue XP on testbed."""
|
|
141
|
+
raise NotImplementedError("TODO")
|
|
142
|
+
|
|
143
|
+
def get_experiment_status(self, id_xp: str) -> str:
|
|
144
|
+
"""Ask server about current state of XP.
|
|
145
|
+
|
|
146
|
+
- after valid submission: disabled / deactivated
|
|
147
|
+
- after scheduling: scheduled
|
|
148
|
+
- before start-time: preparing
|
|
149
|
+
- during run: active
|
|
150
|
+
- after run: post-processing (collecting & assembling data)
|
|
151
|
+
- finished: ready to download
|
|
152
|
+
"""
|
|
153
|
+
raise NotImplementedError("TODO")
|
|
154
|
+
|
|
155
|
+
def get_experiment_results(self, id_xp: str, path: Path) -> bool:
|
|
156
|
+
"""Download resulting files."""
|
|
157
|
+
raise NotImplementedError("TODO")
|
|
@@ -183,12 +183,18 @@ class Fixtures:
|
|
|
183
183
|
else:
|
|
184
184
|
self.file_path = file_path
|
|
185
185
|
self.components: Dict[str, Fixture] = {}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
186
|
+
cache_file = cache_user_path / "fixtures.pickle"
|
|
187
|
+
sheep_detect = Path("/lib/firmware/am335x-pru0-fw").exists()
|
|
188
|
+
|
|
189
|
+
if (
|
|
190
|
+
not sheep_detect
|
|
191
|
+
and cache_file.exists()
|
|
192
|
+
and not file_older_than(cache_file, timedelta(hours=24))
|
|
193
|
+
and not reset
|
|
194
|
+
):
|
|
195
|
+
# speedup by loading from cache
|
|
190
196
|
# TODO: also add version as criterion
|
|
191
|
-
with
|
|
197
|
+
with cache_file.open("rb", buffering=-1) as fd:
|
|
192
198
|
self.components = pickle.load(fd) # noqa: S301
|
|
193
199
|
logger.debug(" -> found & used pickled fixtures")
|
|
194
200
|
else:
|
|
@@ -204,9 +210,9 @@ class Fixtures:
|
|
|
204
210
|
|
|
205
211
|
if len(self.components) < 1:
|
|
206
212
|
logger.error(f"No fixture-components found at {self.file_path.as_posix()}")
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
with
|
|
213
|
+
elif sheep_detect:
|
|
214
|
+
cache_file.parent.mkdir(parents=True, exist_ok=True)
|
|
215
|
+
with cache_file.open("wb", buffering=-1) as fd:
|
|
210
216
|
pickle.dump(self.components, fd)
|
|
211
217
|
|
|
212
218
|
@validate_call
|
shepherd_core/version.py
ADDED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: shepherd_core
|
|
3
|
-
Version: 2024.7.
|
|
3
|
+
Version: 2024.7.4
|
|
4
4
|
Summary: Programming- and CLI-Interface for the h5-dataformat of the Shepherd-Testbed
|
|
5
5
|
Author-email: Ingmar Splitt <ingmar.splitt@tu-dresden.de>
|
|
6
6
|
Maintainer-email: Ingmar Splitt <ingmar.splitt@tu-dresden.de>
|
|
@@ -136,28 +136,30 @@ Notes:
|
|
|
136
136
|
The Library is available via PyPI and can be installed with
|
|
137
137
|
|
|
138
138
|
```shell
|
|
139
|
-
|
|
139
|
+
pip install shepherd-core -U
|
|
140
140
|
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
# or for the full experience (includes core)
|
|
142
|
+
pip install shepherd-data -U
|
|
143
143
|
```
|
|
144
144
|
|
|
145
145
|
For bleeding-edge-features or dev-work it is possible to install directly from GitHub-Sources (here `dev`-branch):
|
|
146
146
|
|
|
147
147
|
```Shell
|
|
148
148
|
pip install git+https://github.com/orgua/shepherd-datalib.git@dev#subdirectory=shepherd_core -U
|
|
149
|
+
# and on sheep with newer debian
|
|
150
|
+
pip install git+https://github.com/orgua/shepherd-datalib.git@dev#subdirectory=shepherd_core -U --break-system-packages
|
|
149
151
|
```
|
|
150
152
|
|
|
151
153
|
If you are working with ``.elf``-files (embedding into experiments) you make "objcopy" accessible to python. In Ubuntu, you can either install ``build-essential`` or ``binutils-$ARCH`` with arch being ``msp430`` or ``arm-none-eabi`` for the nRF52.
|
|
152
154
|
|
|
153
155
|
```shell
|
|
154
|
-
|
|
156
|
+
sudo apt install build-essential
|
|
155
157
|
```
|
|
156
158
|
|
|
157
159
|
For more advanced work with ``.elf``-files (modify value of symbols / target-ID) you should install
|
|
158
160
|
|
|
159
161
|
```shell
|
|
160
|
-
|
|
162
|
+
pip install shepherd-core[elf]
|
|
161
163
|
```
|
|
162
164
|
|
|
163
165
|
and also make sure the prereqs for the [pwntools](https://docs.pwntools.com/en/stable/install.html) are met.
|
|
@@ -165,7 +167,7 @@ and also make sure the prereqs for the [pwntools](https://docs.pwntools.com/en/s
|
|
|
165
167
|
For creating an inventory of the host-system you should install
|
|
166
168
|
|
|
167
169
|
```shell
|
|
168
|
-
|
|
170
|
+
pip install shepherd-core[inventory]
|
|
169
171
|
```
|
|
170
172
|
|
|
171
173
|
## Unittests
|
|
@@ -1,31 +1,32 @@
|
|
|
1
|
-
shepherd_core/__init__.py,sha256=
|
|
2
|
-
shepherd_core/calibration_hw_def.py,sha256=
|
|
1
|
+
shepherd_core/__init__.py,sha256=QyqENyf508XfZQ4vDU5o6UL9rmIqkf8kzwgTF9XU1-Y,1270
|
|
2
|
+
shepherd_core/calibration_hw_def.py,sha256=_nMzgNzSnYyqcLnVCGd4tfA2e0avUXbccjmNpFhiDgo,2830
|
|
3
3
|
shepherd_core/commons.py,sha256=vymKXWcy_1bz7ChzzEATUkJ4p3czCzjIdsSehVjJOY8,218
|
|
4
4
|
shepherd_core/logger.py,sha256=4Q4hTI-nccOZ1_A68fo4UEctfu3pJx3IeHfa9VuDDEo,1804
|
|
5
5
|
shepherd_core/reader.py,sha256=9BuArqou5pmPKUrJH9oiPYlU1DkMxUScL4nftDJuFIs,26790
|
|
6
|
+
shepherd_core/version.py,sha256=nuk8f6h1RBadV7-koFp4yLk1jExaUHnFqMTtxlKZyCo,75
|
|
6
7
|
shepherd_core/writer.py,sha256=xcLCw-YokKaN8TrkwD0IjRmn8xZU0Q8wwWp_1K8JFVY,14475
|
|
7
8
|
shepherd_core/data_models/__init__.py,sha256=IVjKbT2Ilz5bev325EvAuuhd9LfQgQ1u7qKo6dhVA2k,1866
|
|
8
9
|
shepherd_core/data_models/readme.md,sha256=1bdfEypY_0NMhXLxOPRnLAsFca0HuHdq7_01yEWxvUs,2470
|
|
9
10
|
shepherd_core/data_models/virtual_source_doc.txt,sha256=KizMcfGKj7BnHIbaJHT7KeTF01SV__UXv01qV_DGHSs,6057
|
|
10
11
|
shepherd_core/data_models/base/__init__.py,sha256=PSJ6acWViqBm0Eiom8DIgKfFVrp5lzYr8OsDvP79vwI,94
|
|
11
12
|
shepherd_core/data_models/base/cal_measurement.py,sha256=YScPG7QLynbUHdjcznYqU8O5KRh0XiROGxGSk9BETMk,3357
|
|
12
|
-
shepherd_core/data_models/base/calibration.py,sha256=
|
|
13
|
+
shepherd_core/data_models/base/calibration.py,sha256=iy04ajChReqrRNLoNDEH01CM-iCvb5XepBIlyFSWeII,10466
|
|
13
14
|
shepherd_core/data_models/base/content.py,sha256=13j7GSgT73xn27jgDP508thUEJR4U-nCb5n7CJ50c9Y,2463
|
|
14
15
|
shepherd_core/data_models/base/shepherd.py,sha256=DNrx59o1VBuy_liJuUzZRzmTTYB73D_pUWiNyMQyjYY,6112
|
|
15
16
|
shepherd_core/data_models/base/timezone.py,sha256=2T6E46hJ1DAvmqKfu6uIgCK3RSoAKjGXRyzYNaqKyjY,665
|
|
16
|
-
shepherd_core/data_models/base/wrapper.py,sha256=
|
|
17
|
+
shepherd_core/data_models/base/wrapper.py,sha256=Izp17HFCKNAS3TnWcPn3MM9fWdc3A-F7eDyAsYlyWCw,755
|
|
17
18
|
shepherd_core/data_models/content/__init__.py,sha256=wVa5lw6bS-fBgeo-SWydg6rw8AsScxqNgDo81dzteaE,537
|
|
18
19
|
shepherd_core/data_models/content/_external_fixtures.yaml,sha256=0CH7YSWT_hzL-jcg4JjgN9ryQOzbS8S66_pd6GbMnHw,12259
|
|
19
|
-
shepherd_core/data_models/content/energy_environment.py,sha256=
|
|
20
|
+
shepherd_core/data_models/content/energy_environment.py,sha256=bXInmHzlRjBAt7mitig35V-zCfj98ZvGEBio0miSxRg,1425
|
|
20
21
|
shepherd_core/data_models/content/energy_environment_fixture.yaml,sha256=UBXTdGT7MK98zx5w_RBCu-f9uNCKxRgiFBQFbmDUxPc,1301
|
|
21
|
-
shepherd_core/data_models/content/firmware.py,sha256=
|
|
22
|
+
shepherd_core/data_models/content/firmware.py,sha256=MyEiaP6bkOm7i_oihDXTxHC7ajc5aqiIDLn7mhap6YY,5722
|
|
22
23
|
shepherd_core/data_models/content/firmware_datatype.py,sha256=XPU9LOoT3h5qFOlE8WU0vAkw-vymNxzor9kVFyEqsWg,255
|
|
23
24
|
shepherd_core/data_models/content/virtual_harvester.py,sha256=5eEHAZrgHPHZlTxDGaJrckDQgupFNC3Zax67EcCSqR8,9448
|
|
24
25
|
shepherd_core/data_models/content/virtual_harvester_fixture.yaml,sha256=-IRyoQU0HXCEtIIcFmkFdz4snLB7bjFFqNcFVGSMiSA,4332
|
|
25
26
|
shepherd_core/data_models/content/virtual_source.py,sha256=aoD8oam1POid0JG2ppttPA_Jl3y4ko5FNqzoaNKyBD8,14142
|
|
26
27
|
shepherd_core/data_models/content/virtual_source_fixture.yaml,sha256=kx_lpBx0bLKqEHxS09GTnk8kuSbhuGhLgKHeaM6UviE,10481
|
|
27
28
|
shepherd_core/data_models/experiment/__init__.py,sha256=9TE9_aSnCNRhagsIWLTE8XkyjyMGB7kEGdswl-296v0,645
|
|
28
|
-
shepherd_core/data_models/experiment/experiment.py,sha256=
|
|
29
|
+
shepherd_core/data_models/experiment/experiment.py,sha256=wnn6T3czuh4rz6OSYtMltCTbRpPX55TLVAtQcKO7Uhg,4044
|
|
29
30
|
shepherd_core/data_models/experiment/observer_features.py,sha256=qxnb7anuQz9ZW5IUlPdUXYPIl5U7O9uXkJqZtMnAb0Y,5156
|
|
30
31
|
shepherd_core/data_models/experiment/target_config.py,sha256=XIsjbbo7yn_A4q3GMxWbiNzEGA0Kk5gH7-XfQQ7Kg0E,3674
|
|
31
32
|
shepherd_core/data_models/task/__init__.py,sha256=rZLbgqX-dTWY4026-bqW-IWVHbA6C_xP9y0aeRze8FY,3374
|
|
@@ -34,7 +35,7 @@ shepherd_core/data_models/task/firmware_mod.py,sha256=Rw_TA1ykQ7abUd_U0snqZlpZyr
|
|
|
34
35
|
shepherd_core/data_models/task/harvest.py,sha256=HHnqWwRsJupaZJxuohs7NrK6VaDyoRzGOaG2h9y3s1Y,3360
|
|
35
36
|
shepherd_core/data_models/task/observer_tasks.py,sha256=XlH_-EGRrdodTn0c2pjGvpcauc0a9NOnLhysKw8iRwk,3511
|
|
36
37
|
shepherd_core/data_models/task/programming.py,sha256=Mg9_AZHIdG01FheEJAifIRPSB3iZ0UJITf8zeg2jyws,2323
|
|
37
|
-
shepherd_core/data_models/task/testbed_tasks.py,sha256=
|
|
38
|
+
shepherd_core/data_models/task/testbed_tasks.py,sha256=zvIitq0Ek1Ae7baWiBkSQN8nRugyw0N2P4SeVoj_QaY,2090
|
|
38
39
|
shepherd_core/data_models/testbed/__init__.py,sha256=cL3swgijyIpZIW1vl51OVR2seAlWt6Ke9oB_cBkPniU,612
|
|
39
40
|
shepherd_core/data_models/testbed/cape.py,sha256=D23ZKXpZRPIIOMn6LCoJrwHiRbSaYg-y7B6fAt1ap64,1246
|
|
40
41
|
shepherd_core/data_models/testbed/cape_fixture.yaml,sha256=uwZxe6hsqvofn5tzg4sffjbVtTVUkextL1GCri_z2A4,2197
|
|
@@ -46,7 +47,7 @@ shepherd_core/data_models/testbed/observer.py,sha256=hlj6buDzUQKYnlhCJZyxnrAPYKo
|
|
|
46
47
|
shepherd_core/data_models/testbed/observer_fixture.yaml,sha256=w4VS6lTzaVs5IqWjkHanxcjDhIEydQPCV6z_DlsLFqA,4812
|
|
47
48
|
shepherd_core/data_models/testbed/target.py,sha256=KeJaLradQ3oHeeowCg_X0lDHDqyi3R3La0YPKC5Rv90,1838
|
|
48
49
|
shepherd_core/data_models/testbed/target_fixture.yaml,sha256=6YbCV3aTtDUKzC40kPURq9nFwTjT97LNy7imOb_35sk,3668
|
|
49
|
-
shepherd_core/data_models/testbed/testbed.py,sha256=
|
|
50
|
+
shepherd_core/data_models/testbed/testbed.py,sha256=0uJ3OwqCKDn78OCJOaMa2XWxTLF1ultjmpHVSx3LyhE,3695
|
|
50
51
|
shepherd_core/data_models/testbed/testbed_fixture.yaml,sha256=9i2cmYRrHOHTJG9zp40h8h0LgO9DdrCJz8tyGdiQCzc,714
|
|
51
52
|
shepherd_core/decoder_waveform/__init__.py,sha256=-ohGz0fA2tKxUJk4FAQXKtI93d6YGdy0CrkdhOod1QU,120
|
|
52
53
|
shepherd_core/decoder_waveform/uart.py,sha256=sHsXHOsDU1j9zMSZO7CCMTMinT4U_S5NgsEkl1lJK1U,11029
|
|
@@ -59,17 +60,18 @@ shepherd_core/inventory/__init__.py,sha256=nRO11HG4eJ_FaXebSkE0dd1H6qvjrX5n3OQHO
|
|
|
59
60
|
shepherd_core/inventory/python.py,sha256=OWNnyEt0IDPW9XGW-WloU0FExwgZzYNA05VpRj4cZGc,1250
|
|
60
61
|
shepherd_core/inventory/system.py,sha256=jRzko9QNPLaBiG7urVaeqqvb3GtCEYRwc0DAghRkLVo,3159
|
|
61
62
|
shepherd_core/inventory/target.py,sha256=Lq11j25tWieXheOxIDaQb-lc-2omxYVex5P6uGiLUyk,507
|
|
62
|
-
shepherd_core/testbed_client/__init__.py,sha256=
|
|
63
|
+
shepherd_core/testbed_client/__init__.py,sha256=kV-_g1ZiopWg2Aoq0k7fPD0pDqHrMDVYnNOrMBBWgnY,234
|
|
63
64
|
shepherd_core/testbed_client/cache_path.py,sha256=tS0er9on5fw8wddMCt1jkc2uyYOdSTvX_UmfmYJf6tY,445
|
|
64
|
-
shepherd_core/testbed_client/
|
|
65
|
-
shepherd_core/testbed_client/
|
|
65
|
+
shepherd_core/testbed_client/client_abc_fix.py,sha256=BsSkpvJHURRejlS-YPF1f6QRPC_X0fYEsJpinzsx6Jc,4079
|
|
66
|
+
shepherd_core/testbed_client/client_web.py,sha256=iMh5T91152uugbFsqr2vvxLser0KIo5g426dp_6QWUE,5774
|
|
67
|
+
shepherd_core/testbed_client/fixtures.py,sha256=4Uk583R4r6I5IB78HxOn-9UNH3sbFha7OPEdcSXvMCU,9939
|
|
66
68
|
shepherd_core/testbed_client/user_model.py,sha256=5M3vWkAGBwdGDUYAanAjrZwpzMBlh3XLOVvNYWiLmms,2107
|
|
67
69
|
shepherd_core/vsource/__init__.py,sha256=dS33KYLq5GQ9_D8HfdP8iWSocWTghCi2ZZG2AJWNfaM,391
|
|
68
70
|
shepherd_core/vsource/virtual_converter_model.py,sha256=ZSoWVLfRmFEjeCNoQCg3BctzhdfayINUBDU_AJK1CR0,10404
|
|
69
71
|
shepherd_core/vsource/virtual_harvester_model.py,sha256=wCbFfsqDRC5Rfu8qANkmkP9XGJOPHJY9-iSnI850JI4,7817
|
|
70
72
|
shepherd_core/vsource/virtual_source_model.py,sha256=fjN8myTY3I_LpikF_aGAcxes3RGu1GP23P7XKC_UIyA,2737
|
|
71
|
-
shepherd_core-2024.7.
|
|
72
|
-
shepherd_core-2024.7.
|
|
73
|
-
shepherd_core-2024.7.
|
|
74
|
-
shepherd_core-2024.7.
|
|
75
|
-
shepherd_core-2024.7.
|
|
73
|
+
shepherd_core-2024.7.4.dist-info/METADATA,sha256=zFRCTLQEf-XN8Szx8fxb2JwCA6H0F26oUU5UMgWxVaA,7771
|
|
74
|
+
shepherd_core-2024.7.4.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
|
|
75
|
+
shepherd_core-2024.7.4.dist-info/top_level.txt,sha256=wy-t7HRBrKARZxa-Y8_j8d49oVHnulh-95K9ikxVhew,14
|
|
76
|
+
shepherd_core-2024.7.4.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
77
|
+
shepherd_core-2024.7.4.dist-info/RECORD,,
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
"""Client-Class to access a testbed instance."""
|
|
2
|
-
|
|
3
|
-
from importlib import import_module
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import Optional
|
|
6
|
-
from typing import TypedDict
|
|
7
|
-
from typing import Union
|
|
8
|
-
|
|
9
|
-
from pydantic import validate_call
|
|
10
|
-
from typing_extensions import Self
|
|
11
|
-
from typing_extensions import Unpack
|
|
12
|
-
|
|
13
|
-
from ..commons import testbed_server_default
|
|
14
|
-
from ..data_models.base.shepherd import ShpModel
|
|
15
|
-
from ..data_models.base.wrapper import Wrapper
|
|
16
|
-
from .fixtures import Fixtures
|
|
17
|
-
from .user_model import User
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class TestbedClient:
|
|
21
|
-
"""Client-Class to access a testbed instance."""
|
|
22
|
-
|
|
23
|
-
_instance: Optional[Self] = None
|
|
24
|
-
|
|
25
|
-
def __init__(self, server: Optional[str] = None, token: Union[str, Path, None] = None) -> None:
|
|
26
|
-
if not hasattr(self, "_token"):
|
|
27
|
-
self._token: str = "null"
|
|
28
|
-
self._server: Optional[str] = testbed_server_default
|
|
29
|
-
self._user: Optional[User] = None
|
|
30
|
-
self._key: Optional[str] = None
|
|
31
|
-
self._fixtures: Optional[Fixtures] = Fixtures()
|
|
32
|
-
self._connected: bool = False
|
|
33
|
-
self._req = None
|
|
34
|
-
if server is not None:
|
|
35
|
-
self.connect(server=server, token=token)
|
|
36
|
-
|
|
37
|
-
@classmethod
|
|
38
|
-
def __new__(cls, *_args: tuple, **_kwargs: Unpack[TypedDict]) -> Self:
|
|
39
|
-
if cls._instance is None:
|
|
40
|
-
cls._instance = object.__new__(cls)
|
|
41
|
-
return cls._instance
|
|
42
|
-
|
|
43
|
-
def __del__(self) -> None:
|
|
44
|
-
TestbedClient._instance = None
|
|
45
|
-
|
|
46
|
-
@validate_call
|
|
47
|
-
def connect(self, server: Optional[str] = None, token: Union[str, Path, None] = None) -> bool:
|
|
48
|
-
"""Establish connection to testbed-server.
|
|
49
|
-
|
|
50
|
-
server: either "local" to use demo-fixtures or something like "https://HOST:PORT"
|
|
51
|
-
token: your account validation.
|
|
52
|
-
"""
|
|
53
|
-
if isinstance(token, Path):
|
|
54
|
-
with token.resolve().open() as file:
|
|
55
|
-
self._token = file.read()
|
|
56
|
-
elif isinstance(token, str):
|
|
57
|
-
self._token = token
|
|
58
|
-
|
|
59
|
-
if server:
|
|
60
|
-
self._server = server.lower()
|
|
61
|
-
|
|
62
|
-
if self._server:
|
|
63
|
-
self._req = import_module("requests") # here due to slow startup
|
|
64
|
-
|
|
65
|
-
# extended connection-test:
|
|
66
|
-
self._query_session_key()
|
|
67
|
-
self._connected = True
|
|
68
|
-
return self._query_user_data()
|
|
69
|
-
|
|
70
|
-
return True
|
|
71
|
-
|
|
72
|
-
def insert(self, data: ShpModel) -> bool:
|
|
73
|
-
wrap = Wrapper(
|
|
74
|
-
datatype=type(data).__name__,
|
|
75
|
-
parameters=data.model_dump(),
|
|
76
|
-
)
|
|
77
|
-
if self._connected:
|
|
78
|
-
r = self._req.post(self._server + "/add", data=wrap.model_dump_json(), timeout=2)
|
|
79
|
-
r.raise_for_status()
|
|
80
|
-
else:
|
|
81
|
-
self._fixtures.insert_model(wrap)
|
|
82
|
-
return True
|
|
83
|
-
|
|
84
|
-
def query_ids(self, model_type: str) -> list:
|
|
85
|
-
if self._connected:
|
|
86
|
-
raise NotImplementedError("TODO")
|
|
87
|
-
return list(self._fixtures[model_type].elements_by_id.keys())
|
|
88
|
-
|
|
89
|
-
def query_names(self, model_type: str) -> list:
|
|
90
|
-
if self._connected:
|
|
91
|
-
raise NotImplementedError("TODO")
|
|
92
|
-
return list(self._fixtures[model_type].elements_by_name.keys())
|
|
93
|
-
|
|
94
|
-
def query_item(
|
|
95
|
-
self, model_type: str, uid: Optional[int] = None, name: Optional[str] = None
|
|
96
|
-
) -> dict:
|
|
97
|
-
if self._connected:
|
|
98
|
-
raise NotImplementedError("TODO")
|
|
99
|
-
if uid is not None:
|
|
100
|
-
return self._fixtures[model_type].query_id(uid)
|
|
101
|
-
if name is not None:
|
|
102
|
-
return self._fixtures[model_type].query_name(name)
|
|
103
|
-
raise ValueError("Query needs either uid or name of object")
|
|
104
|
-
|
|
105
|
-
def _query_session_key(self) -> bool:
|
|
106
|
-
if self._server:
|
|
107
|
-
r = self._req.get(self._server + "/session_key", timeout=2)
|
|
108
|
-
r.raise_for_status()
|
|
109
|
-
self._key = r.json()["value"] # TODO: not finished
|
|
110
|
-
return True
|
|
111
|
-
return False
|
|
112
|
-
|
|
113
|
-
def _query_user_data(self) -> bool:
|
|
114
|
-
if self._server:
|
|
115
|
-
r = self._req.get(self._server + "/user?token=" + self._token, timeout=2)
|
|
116
|
-
# TODO: possibly a security nightmare (send via json or encrypted via public key?)
|
|
117
|
-
r.raise_for_status()
|
|
118
|
-
self._user = User(**r.json())
|
|
119
|
-
return True
|
|
120
|
-
return False
|
|
121
|
-
|
|
122
|
-
def try_inheritance(self, model_type: str, values: dict) -> (dict, list):
|
|
123
|
-
if self._connected:
|
|
124
|
-
raise NotImplementedError("TODO")
|
|
125
|
-
return self._fixtures[model_type].inheritance(values)
|
|
126
|
-
|
|
127
|
-
def try_completing_model(self, model_type: str, values: dict) -> (dict, list):
|
|
128
|
-
"""Init by name/id, for none existing instances raise Exception."""
|
|
129
|
-
if len(values) == 1 and next(iter(values.keys())) in {"id", "name"}:
|
|
130
|
-
value = next(iter(values.values()))
|
|
131
|
-
if (
|
|
132
|
-
isinstance(value, str)
|
|
133
|
-
and value.lower() in self._fixtures[model_type].elements_by_name
|
|
134
|
-
):
|
|
135
|
-
values = self.query_item(model_type, name=value)
|
|
136
|
-
elif isinstance(value, int) and value in self._fixtures[model_type].elements_by_id:
|
|
137
|
-
# TODO: still depending on _fixture
|
|
138
|
-
values = self.query_item(model_type, uid=value)
|
|
139
|
-
else:
|
|
140
|
-
msg = f"Query {model_type} by name / ID failed - {values} is unknown!"
|
|
141
|
-
raise ValueError(msg)
|
|
142
|
-
return self.try_inheritance(model_type, values)
|
|
143
|
-
|
|
144
|
-
def fill_in_user_data(self, values: dict) -> dict:
|
|
145
|
-
if self._user:
|
|
146
|
-
# TODO: this looks wrong, should have "is None", why not always overwrite?
|
|
147
|
-
if values.get("owner"):
|
|
148
|
-
values["owner"] = self._user.name
|
|
149
|
-
if values.get("group"):
|
|
150
|
-
values["group"] = self._user.group
|
|
151
|
-
|
|
152
|
-
# hotfix until testbed.client is working, TODO
|
|
153
|
-
if values.get("owner") is None:
|
|
154
|
-
values["owner"] = "unknown"
|
|
155
|
-
if values.get("group") is None:
|
|
156
|
-
values["group"] = "unknown"
|
|
157
|
-
|
|
158
|
-
return values
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
tb_client = TestbedClient()
|
|
File without changes
|
|
File without changes
|