shepherd-core 2023.12.1__py3-none-any.whl → 2024.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. shepherd_core/__init__.py +3 -2
  2. shepherd_core/data_models/base/calibration.py +3 -1
  3. shepherd_core/data_models/base/content.py +11 -4
  4. shepherd_core/data_models/base/shepherd.py +2 -0
  5. shepherd_core/data_models/content/energy_environment.py +1 -1
  6. shepherd_core/data_models/content/virtual_harvester.py +1 -1
  7. shepherd_core/data_models/content/virtual_source.py +18 -13
  8. shepherd_core/data_models/experiment/experiment.py +10 -10
  9. shepherd_core/data_models/experiment/observer_features.py +5 -6
  10. shepherd_core/data_models/task/observer_tasks.py +1 -1
  11. shepherd_core/data_models/task/testbed_tasks.py +7 -3
  12. shepherd_core/data_models/testbed/cape.py +1 -1
  13. shepherd_core/data_models/testbed/gpio.py +1 -1
  14. shepherd_core/data_models/testbed/mcu.py +1 -1
  15. shepherd_core/data_models/testbed/observer.py +1 -1
  16. shepherd_core/data_models/testbed/target.py +1 -1
  17. shepherd_core/data_models/testbed/testbed.py +1 -1
  18. shepherd_core/decoder_waveform/uart.py +1 -0
  19. shepherd_core/fw_tools/validation.py +1 -2
  20. shepherd_core/inventory/__init__.py +51 -1
  21. shepherd_core/inventory/system.py +8 -0
  22. shepherd_core/reader.py +19 -16
  23. shepherd_core/testbed_client/cache_path.py +15 -0
  24. shepherd_core/testbed_client/client.py +2 -3
  25. shepherd_core/testbed_client/fixtures.py +4 -1
  26. shepherd_core/testbed_client/user_model.py +6 -4
  27. shepherd_core/vsource/virtual_converter_model.py +1 -1
  28. shepherd_core/vsource/virtual_harvester_model.py +1 -0
  29. shepherd_core/vsource/virtual_source_model.py +1 -0
  30. shepherd_core/writer.py +5 -2
  31. {shepherd_core-2023.12.1.dist-info → shepherd_core-2024.4.1.dist-info}/METADATA +49 -34
  32. shepherd_core-2024.4.1.dist-info/RECORD +64 -0
  33. {shepherd_core-2023.12.1.dist-info → shepherd_core-2024.4.1.dist-info}/WHEEL +1 -1
  34. {shepherd_core-2023.12.1.dist-info → shepherd_core-2024.4.1.dist-info}/top_level.txt +0 -1
  35. shepherd_core/data_models/content/_external_fixtures.yaml +0 -394
  36. shepherd_core/data_models/content/energy_environment_fixture.yaml +0 -50
  37. shepherd_core/data_models/content/virtual_harvester_fixture.yaml +0 -159
  38. shepherd_core/data_models/content/virtual_source_fixture.yaml +0 -229
  39. shepherd_core/data_models/testbed/cape_fixture.yaml +0 -94
  40. shepherd_core/data_models/testbed/gpio_fixture.yaml +0 -166
  41. shepherd_core/data_models/testbed/mcu_fixture.yaml +0 -19
  42. shepherd_core/data_models/testbed/observer_fixture.yaml +0 -220
  43. shepherd_core/data_models/testbed/target_fixture.yaml +0 -137
  44. shepherd_core/data_models/testbed/testbed_fixture.yaml +0 -25
  45. shepherd_core-2023.12.1.dist-info/RECORD +0 -117
  46. tests/__init__.py +0 -0
  47. tests/conftest.py +0 -64
  48. tests/data_models/__init__.py +0 -0
  49. tests/data_models/conftest.py +0 -14
  50. tests/data_models/example_cal_data.yaml +0 -31
  51. tests/data_models/example_cal_data_faulty.yaml +0 -29
  52. tests/data_models/example_cal_meas.yaml +0 -178
  53. tests/data_models/example_cal_meas_faulty1.yaml +0 -142
  54. tests/data_models/example_cal_meas_faulty2.yaml +0 -136
  55. tests/data_models/example_config_emulator.yaml +0 -41
  56. tests/data_models/example_config_experiment.yaml +0 -16
  57. tests/data_models/example_config_experiment_alternative.yaml +0 -14
  58. tests/data_models/example_config_harvester.yaml +0 -15
  59. tests/data_models/example_config_testbed.yaml +0 -26
  60. tests/data_models/example_config_virtsource.yaml +0 -78
  61. tests/data_models/test_base_models.py +0 -205
  62. tests/data_models/test_content_fixtures.py +0 -41
  63. tests/data_models/test_content_models.py +0 -282
  64. tests/data_models/test_examples.py +0 -48
  65. tests/data_models/test_experiment_models.py +0 -277
  66. tests/data_models/test_task_generation.py +0 -52
  67. tests/data_models/test_task_models.py +0 -131
  68. tests/data_models/test_testbed_fixtures.py +0 -47
  69. tests/data_models/test_testbed_models.py +0 -187
  70. tests/decoder_waveform/__init__.py +0 -0
  71. tests/decoder_waveform/test_decoder.py +0 -34
  72. tests/fw_tools/__init__.py +0 -0
  73. tests/fw_tools/conftest.py +0 -5
  74. tests/fw_tools/test_converter.py +0 -76
  75. tests/fw_tools/test_patcher.py +0 -66
  76. tests/fw_tools/test_validation.py +0 -56
  77. tests/inventory/__init__.py +0 -0
  78. tests/inventory/test_inventory.py +0 -20
  79. tests/test_cal_hw.py +0 -34
  80. tests/test_examples.py +0 -40
  81. tests/test_logger.py +0 -15
  82. tests/test_reader.py +0 -283
  83. tests/test_writer.py +0 -169
  84. tests/testbed_client/__init__.py +0 -0
  85. tests/vsource/__init__.py +0 -0
  86. tests/vsource/conftest.py +0 -49
  87. tests/vsource/test_converter.py +0 -161
  88. tests/vsource/test_harvester.py +0 -73
  89. tests/vsource/test_z.py +0 -5
  90. {shepherd_core-2023.12.1.dist-info → shepherd_core-2024.4.1.dist-info}/zip-safe +0 -0
shepherd_core/__init__.py CHANGED
@@ -4,6 +4,7 @@ Provides classes for storing and retrieving sampled IV data to/from
4
4
  HDF5 files.
5
5
 
6
6
  """
7
+
7
8
  from .data_models.base.calibration import Calc_t
8
9
  from .data_models.base.calibration import CalibrationCape
9
10
  from .data_models.base.calibration import CalibrationEmulator
@@ -12,7 +13,7 @@ from .data_models.base.calibration import CalibrationPair
12
13
  from .data_models.base.calibration import CalibrationSeries
13
14
  from .data_models.base.timezone import local_now
14
15
  from .data_models.base.timezone import local_tz
15
- from .data_models.task import Compression
16
+ from .data_models.task.emulation import Compression
16
17
  from .inventory import Inventory
17
18
  from .logger import get_verbose_level
18
19
  from .logger import increase_verbose_level
@@ -22,7 +23,7 @@ from .testbed_client.client import TestbedClient
22
23
  from .testbed_client.client import tb_client
23
24
  from .writer import Writer
24
25
 
25
- __version__ = "2023.12.1"
26
+ __version__ = "2024.04.1"
26
27
 
27
28
  __all__ = [
28
29
  "Reader",
@@ -165,7 +165,7 @@ class CapeData(ShpModel):
165
165
  `See<https://github.com/beagleboard/beaglebone-black/wiki/System-Reference-Manual#824_EEPROM_Data_Format>`_
166
166
  """
167
167
 
168
- header: conbytes(max_length=4) = b"\xAA\x55\x33\xEE"
168
+ header: conbytes(max_length=4) = b"\xaa\x55\x33\xee"
169
169
  eeprom_revision: constr(max_length=2) = "A2"
170
170
  board_name: constr(max_length=32) = "BeagleBone SHEPHERD2 Cape"
171
171
  version: constr(max_length=4) = "24B0"
@@ -207,6 +207,7 @@ class CalibrationCape(ShpModel):
207
207
  cape: data can be supplied
208
208
  Returns:
209
209
  CalibrationCape object with extracted calibration data.
210
+
210
211
  """
211
212
  dv = cls().model_dump(include={"harvester", "emulator"})
212
213
  lw = list(dict_generator(dv))
@@ -225,6 +226,7 @@ class CalibrationCape(ShpModel):
225
226
  Returns
226
227
  -------
227
228
  Byte string representation of calibration values.
229
+
228
230
  """
229
231
  lw = list(dict_generator(self.model_dump(include={"harvester", "emulator"})))
230
232
  values = [walk[-1] for walk in lw]
@@ -1,7 +1,10 @@
1
1
  import hashlib
2
2
  from datetime import datetime
3
3
  from typing import Optional
4
+ from typing import Union
5
+ from uuid import uuid4
4
6
 
7
+ from pydantic import UUID4
5
8
  from pydantic import Field
6
9
  from pydantic import StringConstraints
7
10
  from pydantic import model_validator
@@ -14,30 +17,34 @@ from .timezone import local_now
14
17
  # constr -> to_lower=True, max_length=16, regex=r"^[\w]+$"
15
18
  # ⤷ Regex = AlphaNum
16
19
  IdInt = Annotated[int, Field(ge=0, lt=2**128)]
17
- NameStr = Annotated[str, StringConstraints(max_length=32, pattern=r'^[^<>:;,?"*|\/\\]+$')]
20
+ NameStr = Annotated[str, StringConstraints(max_length=32, pattern=r"^[^<>:;,?\"\*|\/\\]+$")]
18
21
  # ⤷ Regex = FileSystem-Compatible ASCII
19
22
  SafeStr = Annotated[str, StringConstraints(pattern=r"^[ -~]+$")]
20
23
  # ⤷ Regex = All Printable ASCII-Characters with Space
21
24
 
22
25
 
23
26
  def id_default() -> int:
27
+ # note: IdInt has space for 128 bit, so 128/4 = 32 hex-chars
24
28
  time_stamp = str(local_now()).encode("utf-8")
25
- time_hash = hashlib.sha3_224(time_stamp).hexdigest()[-16:]
29
+ time_hash = hashlib.sha3_224(time_stamp).hexdigest()[-32:]
26
30
  return int(time_hash, 16)
27
31
 
28
32
 
29
33
  class ContentModel(ShpModel):
30
34
  # General Properties
31
- id: IdInt = Field( # noqa: A003
35
+ # id: UUID4 = Field( # TODO: db-migration - temp fix for documentation
36
+ id: Union[UUID4, int] = Field(
32
37
  description="Unique ID",
33
- default_factory=id_default,
38
+ default_factory=uuid4,
34
39
  )
35
40
  name: NameStr
36
41
  description: Annotated[Optional[SafeStr], Field(description="Required when public")] = None
37
42
  comment: Optional[SafeStr] = None
38
43
  created: datetime = Field(default_factory=datetime.now)
44
+ updated_last: datetime = Field(default_factory=datetime.now)
39
45
 
40
46
  # Ownership & Access
47
+ # TODO: remove owner & group, only needed for DB
41
48
  owner: NameStr
42
49
  group: Annotated[NameStr, Field(description="University or Subgroup")]
43
50
  visible2group: bool = False
@@ -7,6 +7,7 @@ from typing import Any
7
7
  from typing import Generator
8
8
  from typing import Optional
9
9
  from typing import Union
10
+ from uuid import UUID
10
11
 
11
12
  import yaml
12
13
  from pydantic import BaseModel
@@ -39,6 +40,7 @@ yaml.add_representer(pathlib.WindowsPath, path2str, SafeDumper)
39
40
  yaml.add_representer(pathlib.Path, path2str, SafeDumper)
40
41
  yaml.add_representer(timedelta, time2int, SafeDumper)
41
42
  yaml.add_representer(IPv4Address, generic2str, SafeDumper)
43
+ yaml.add_representer(UUID, generic2str, SafeDumper)
42
44
 
43
45
 
44
46
  class ShpModel(BaseModel):
@@ -13,7 +13,7 @@ class EnergyDType(str, Enum):
13
13
  ivsample = "ivsample"
14
14
  ivsamples = "ivsample"
15
15
  ivcurve = "ivcurve"
16
- ivcurves = "ivcurve"
16
+ ivcurves = "ivcurve" # a stream could also be called iv-surface
17
17
  isc_voc = "isc_voc"
18
18
 
19
19
 
@@ -76,7 +76,7 @@ class VirtualHarvesterConfig(ContentModel, title="Config for the Harvester"):
76
76
  logger.debug("VHrv-Inheritances: %s", chain)
77
77
 
78
78
  # post corrections -> should be in separate validator
79
- cal = CalibrationHarvester() # todo: as argument?
79
+ cal = CalibrationHarvester() # TODO: as argument?
80
80
  c_limit = values.get("current_limit_uA", 50_000) # cls.current_limit_uA)
81
81
  values["current_limit_uA"] = max(10**6 * cal.adc_C_Hrv.raw_to_si(4), c_limit)
82
82
 
@@ -25,12 +25,13 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
25
25
  for supplying the Target Node during the experiment.
26
26
  If not already done, the energy will be harvested and then converted.
27
27
  The converter-stage is software defined and offers:
28
- buck-boost-combinations,
29
- a simple diode + resistor and
30
- an intermediate buffer capacitor.
31
- TODO: I,V,R should be in regular unit (V, A, Ohm)
28
+ - buck-boost-combinations,
29
+ - a simple diode + resistor and
30
+ - an intermediate buffer capacitor.
32
31
  """
33
32
 
33
+ # TODO: I,V,R should be in regular unit (V, A, Ohm)
34
+
34
35
  # General Metadata & Ownership -> ContentModel
35
36
 
36
37
  enable_boost: bool = False
@@ -123,14 +124,17 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
123
124
  only has simpler formula, second enabling when V_Cap >= V_out
124
125
 
125
126
  Math behind this calculation:
126
- Energy-Change Storage Cap -> E_new = E_old - E_output
127
- with Energy of a Cap -> E_x = C_x * V_x^2 / 2
128
- combine formulas ->
129
- C_store * V_store_new^2 / 2 = C_store * V_store_old^2 / 2 - C_out * V_out^2 / 2
130
- convert formula to V_new -> V_store_new^2 = V_store_old^2 - (C_out / C_store) * V_out^2
131
- convert into dV -> dV = V_store_new - V_store_old
132
- in case of V_cap = V_out -> dV = V_store_old * (sqrt(1 - C_out / C_store) - 1)
133
- -> dV values will be reversed (negated), because dV is always negative (Voltage drop)
127
+
128
+ - Energy-Change Storage Cap -> E_new = E_old - E_output
129
+ - with Energy of a Cap -> E_x = C_x * V_x^2 / 2
130
+ - combine formulas -> C_store * V_store_new^2 / 2 =
131
+ C_store * V_store_old^2 / 2 - C_out * V_out^2 / 2
132
+ - convert formula to V_new -> V_store_new^2 =
133
+ V_store_old^2 - (C_out / C_store) * V_out^2
134
+ - convert into dV -> dV = V_store_new - V_store_old
135
+ - in case of V_cap = V_out -> dV = V_store_old * (sqrt(1 - C_out / C_store) - 1)
136
+
137
+ Note: dV values will be reversed (negated), because dV is always negative (Voltage drop)
134
138
  """
135
139
  values = {}
136
140
  if self.C_intermediate_uF > 0 and self.C_output_uF > 0:
@@ -186,8 +190,9 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
186
190
 
187
191
  def calc_converter_mode(self, *, log_intermediate_node: bool) -> int:
188
192
  """Assembles bitmask from discrete values
193
+
189
194
  log_intermediate_node: record / log virtual intermediate (cap-)voltage and
190
- -current (out) instead of output-voltage and -current
195
+ -current (out) instead of output-voltage and -current
191
196
  """
192
197
  enable_storage = self.C_intermediate_uF > 0
193
198
  enable_boost = self.enable_boost and enable_storage
@@ -2,8 +2,10 @@ from datetime import datetime
2
2
  from datetime import timedelta
3
3
  from typing import List
4
4
  from typing import Optional
5
+ from typing import Union
6
+ from uuid import uuid4
5
7
 
6
- from pydantic import EmailStr
8
+ from pydantic import UUID4
7
9
  from pydantic import Field
8
10
  from pydantic import model_validator
9
11
  from typing_extensions import Annotated
@@ -12,7 +14,6 @@ from typing_extensions import Self
12
14
  from ..base.content import IdInt
13
15
  from ..base.content import NameStr
14
16
  from ..base.content import SafeStr
15
- from ..base.content import id_default
16
17
  from ..base.shepherd import ShpModel
17
18
  from ..testbed.target import Target
18
19
  from ..testbed.testbed import Testbed
@@ -26,11 +27,10 @@ class Experiment(ShpModel, title="Config of an Experiment"):
26
27
  """
27
28
 
28
29
  # General Properties
29
- id: IdInt = Field( # noqa: A003
30
- description="Unique ID",
31
- default_factory=id_default,
32
- )
30
+ # id: UUID4 ... # TODO db-migration - temp fix for documentation
31
+ id: Union[UUID4, int] = Field(default_factory=uuid4)
33
32
  # ⤷ TODO: automatic ID is problematic for identification by hash
33
+
34
34
  name: NameStr
35
35
  description: Annotated[
36
36
  Optional[SafeStr], Field(description="Required for public instances")
@@ -38,12 +38,12 @@ class Experiment(ShpModel, title="Config of an Experiment"):
38
38
  comment: Optional[SafeStr] = None
39
39
  created: datetime = Field(default_factory=datetime.now)
40
40
 
41
- # Ownership & Access, TODO
42
- owner_id: Optional[IdInt] = 5472 # UUID?
41
+ # Ownership & Access
42
+ owner_id: Optional[IdInt] = None
43
43
 
44
44
  # feedback
45
- email_results: Optional[EmailStr] = None
46
- # ⤷ TODO: can be bool, as its linked to account
45
+ email_results: bool = False
46
+
47
47
  sys_logging: SystemLogging = SystemLogging(dmesg=True, ptp=True, shepherd=True)
48
48
 
49
49
  # schedule
@@ -66,7 +66,7 @@ class GpioTracing(ShpModel, title="Config for GPIO-Tracing"):
66
66
 
67
67
  # post-processing,
68
68
  uart_decode: bool = False
69
- # todo: quickfix - uart-log currently done online in userspace
69
+ # TODO: quickfix - uart-log currently done online in userspace
70
70
  # NOTE: gpio-tracing currently shows rather big - but rare - "blind" windows (~1-4us)
71
71
  uart_pin: GPIO = GPIO(name="GPIO8")
72
72
  uart_baudrate: Annotated[int, Field(ge=2_400, le=921_600)] = 115_200
@@ -113,11 +113,10 @@ class GpioEvent(ShpModel, title="Config for a GPIO-Event"):
113
113
 
114
114
 
115
115
  class GpioActuation(ShpModel, title="Config for GPIO-Actuation"):
116
- """Configuration for a GPIO-Actuation-Sequence
117
- TODO: not implemented ATM:
118
- - decide if pru control sys-gpio or
119
- - reverses pru-gpio (preferred if possible)
120
- """
116
+ """Configuration for a GPIO-Actuation-Sequence"""
117
+
118
+ # TODO: not implemented ATM - decide if pru control sys-gpio or
119
+ # TODO: not implemented ATM - reverses pru-gpio (preferred if possible)
121
120
 
122
121
  events: Annotated[List[GpioEvent], Field(min_length=1, max_length=1024)]
123
122
 
@@ -21,7 +21,7 @@ class ObserverTasks(ShpModel):
21
21
  """Collection of tasks for selected observer included in experiment"""
22
22
 
23
23
  observer: NameStr
24
- owner_id: IdInt
24
+ owner_id: Optional[IdInt] # TODO: set to optional for now, shouldn't be
25
25
 
26
26
  # PRE PROCESS
27
27
  time_prep: datetime # TODO: should be optional
@@ -1,12 +1,12 @@
1
1
  from typing import List
2
2
  from typing import Optional
3
3
 
4
- from pydantic import EmailStr
5
4
  from pydantic import Field
6
5
  from pydantic import validate_call
7
6
  from typing_extensions import Annotated
8
7
  from typing_extensions import Self
9
8
 
9
+ from ..base.content import IdInt
10
10
  from ..base.content import NameStr
11
11
  from ..base.shepherd import ShpModel
12
12
  from ..experiment.experiment import Experiment
@@ -21,7 +21,10 @@ class TestbedTasks(ShpModel):
21
21
  observer_tasks: Annotated[List[ObserverTasks], Field(min_length=1, max_length=128)]
22
22
 
23
23
  # POST PROCESS
24
- email: Optional[EmailStr] = None
24
+ email_results: bool = False
25
+ owner_id: Optional[IdInt]
26
+ # TODO: had real email previously, does it really need these at all?
27
+ # DB stores experiment and knows when to email
25
28
 
26
29
  @classmethod
27
30
  @validate_call
@@ -34,7 +37,8 @@ class TestbedTasks(ShpModel):
34
37
  return cls(
35
38
  name=xp.name,
36
39
  observer_tasks=obs_tasks,
37
- email=xp.email_results,
40
+ email_results=xp.email_results,
41
+ owner_id=xp.owner_id,
38
42
  )
39
43
 
40
44
  def get_observer_tasks(self, observer: str) -> Optional[ObserverTasks]:
@@ -24,7 +24,7 @@ class TargetPort(str, Enum):
24
24
  class Cape(ShpModel, title="Shepherd-Cape"):
25
25
  """meta-data representation of a testbed-component (physical object)"""
26
26
 
27
- id: IdInt # noqa: A003
27
+ id: IdInt
28
28
  name: NameStr
29
29
  version: NameStr
30
30
  description: SafeStr
@@ -26,7 +26,7 @@ class Direction(str, Enum):
26
26
  class GPIO(ShpModel, title="GPIO of Observer Node"):
27
27
  """meta-data representation of a testbed-component"""
28
28
 
29
- id: IdInt # noqa: A003
29
+ id: IdInt
30
30
  name: NameStr
31
31
  description: Optional[SafeStr] = None
32
32
  comment: Optional[SafeStr] = None
@@ -26,7 +26,7 @@ class ProgrammerProtocol(str, Enum):
26
26
  class MCU(ShpModel, title="Microcontroller of the Target Node"):
27
27
  """meta-data representation of a testbed-component (physical object)"""
28
28
 
29
- id: IdInt # noqa: A003
29
+ id: IdInt
30
30
  name: NameStr
31
31
  description: SafeStr
32
32
  comment: Optional[SafeStr] = None
@@ -26,7 +26,7 @@ MACStr = Annotated[
26
26
  class Observer(ShpModel, title="Shepherd-Sheep"):
27
27
  """meta-data representation of a testbed-component (physical object)"""
28
28
 
29
- id: IdInt # noqa: A003
29
+ id: IdInt
30
30
  name: NameStr
31
31
  description: SafeStr
32
32
  comment: Optional[SafeStr] = None
@@ -21,7 +21,7 @@ MCUPort = Annotated[int, Field(ge=1, le=2)]
21
21
  class Target(ShpModel, title="Target Node (DuT)"):
22
22
  """meta-data representation of a testbed-component (physical object)"""
23
23
 
24
- id: IdInt # noqa: A003
24
+ id: IdInt
25
25
  name: NameStr
26
26
  version: NameStr
27
27
  description: SafeStr
@@ -20,7 +20,7 @@ from .observer import Observer
20
20
  class Testbed(ShpModel):
21
21
  """meta-data representation of a testbed-component (physical object)"""
22
22
 
23
- id: IdInt # noqa: A003
23
+ id: IdInt
24
24
  name: NameStr
25
25
  description: SafeStr
26
26
  comment: Optional[SafeStr] = None
@@ -17,6 +17,7 @@ https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter
17
17
  https://sigrok.org/wiki/Protocol_decoder:Uart
18
18
 
19
19
  """
20
+
20
21
  from enum import Enum
21
22
  from pathlib import Path
22
23
  from typing import Optional
@@ -1,6 +1,5 @@
1
- """TODO: Work in Progress
1
+ """TODO: Work in Progress"""
2
2
 
3
- """
4
3
  import tempfile
5
4
  from pathlib import Path
6
5
 
@@ -3,6 +3,9 @@
3
3
  - system-parameters
4
4
  - hardware-config
5
5
  """
6
+
7
+ from datetime import datetime
8
+ from datetime import timedelta
6
9
  from pathlib import Path
7
10
  from typing import List
8
11
 
@@ -26,14 +29,20 @@ __all__ = [
26
29
 
27
30
  class Inventory(PythonInventory, SystemInventory, TargetInventory):
28
31
  # has all child-parameters
32
+ hostname: str
33
+ created: datetime
29
34
 
30
35
  @classmethod
31
36
  def collect(cls) -> Self:
32
37
  # one by one for more precise error messages
33
- pid = PythonInventory.collect().model_dump(exclude_unset=True, exclude_defaults=True)
38
+ # NOTE: system is first, as it must take a precise timestamp
34
39
  sid = SystemInventory.collect().model_dump(exclude_unset=True, exclude_defaults=True)
40
+ pid = PythonInventory.collect().model_dump(exclude_unset=True, exclude_defaults=True)
35
41
  tid = TargetInventory.collect().model_dump(exclude_unset=True, exclude_defaults=True)
36
42
  model = {**pid, **sid, **tid}
43
+ # make important metadata available at root level
44
+ model["created"] = sid["timestamp"]
45
+ model["hostname"] = sid["hostname"]
37
46
  return cls(**model)
38
47
 
39
48
 
@@ -53,3 +62,44 @@ class InventoryList(ShpModel):
53
62
  content = list(item.model_dump().values())
54
63
  content = ["" if value is None else str(value) for value in content]
55
64
  fd.write(", ".join(content) + "\r\n")
65
+
66
+ def warn(self) -> dict:
67
+ warnings = {}
68
+ ts_earl = min([_e.created.timestamp() for _e in self.elements])
69
+ for _e in self.elements:
70
+ if _e.uptime > timedelta(hours=30).total_seconds():
71
+ warnings["uptime"] = f"[{self.hostname}] restart is recommended"
72
+ if (_e.created.timestamp() - ts_earl) > 10:
73
+ warnings["time_delta"] = f"[{self.hostname}] time-sync has failed"
74
+
75
+ # turn dict[hostname][type] = val
76
+ # to dict[type][val] = list[hostnames]
77
+ _inp = {
78
+ _e.hostname: _e.model_dump(exclude_unset=True, exclude_defaults=True)
79
+ for _e in self.elements
80
+ }
81
+ result = {}
82
+ for _host, _types in _inp.items():
83
+ for _type, _val in _types.items():
84
+ if _type not in result:
85
+ result[_type] = {}
86
+ if _val not in result[_type]:
87
+ result[_type][_val] = []
88
+ result[_type][_val].append(_host)
89
+ rescnt = {_key: len(_val) for _key, _val in result.items()}
90
+ t_unique = [
91
+ "h5py",
92
+ "numpy",
93
+ "pydantic",
94
+ "python",
95
+ "shepherd_core",
96
+ "shepherd_sheep",
97
+ "yaml",
98
+ "zstandard",
99
+ ]
100
+ for _key in t_unique:
101
+ if rescnt[_key] > 1:
102
+ warnings[_key] = f"[{_key}] VersionMismatch - {result[_key]}"
103
+
104
+ # TODO: finish with more potential warnings
105
+ return warnings
@@ -2,10 +2,12 @@ import platform
2
2
  import subprocess
3
3
  import time
4
4
  from contextlib import suppress
5
+ from datetime import datetime
5
6
  from typing import Optional
6
7
 
7
8
  from typing_extensions import Self
8
9
 
10
+ from .. import local_now
9
11
  from .. import logger
10
12
 
11
13
  try:
@@ -22,6 +24,9 @@ from ..data_models import ShpModel
22
24
  class SystemInventory(ShpModel):
23
25
  uptime: PositiveInt
24
26
  # ⤷ seconds
27
+ timestamp: datetime
28
+ # time_delta: timedelta = timedelta(0) # noqa: ERA001
29
+ # ⤷ lag behind earliest observer, TODO: wrong place
25
30
 
26
31
  system: str
27
32
  release: str
@@ -43,6 +48,8 @@ class SystemInventory(ShpModel):
43
48
 
44
49
  @classmethod
45
50
  def collect(cls) -> Self:
51
+ ts = local_now()
52
+
46
53
  if psutil is None:
47
54
  ifs2 = {}
48
55
  uptime = 0
@@ -58,6 +65,7 @@ class SystemInventory(ShpModel):
58
65
 
59
66
  model_dict = {
60
67
  "uptime": round(uptime),
68
+ "timestamp": ts,
61
69
  "system": platform.system(),
62
70
  "release": platform.release(),
63
71
  "version": platform.version(),
shepherd_core/reader.py CHANGED
@@ -1,6 +1,5 @@
1
- """
2
- Reader-Baseclass
3
- """
1
+ """Reader-Baseclass"""
2
+
4
3
  import contextlib
5
4
  import errno
6
5
  import logging
@@ -36,8 +35,10 @@ class Reader:
36
35
  """Sequentially Reads shepherd-data from HDF5 file.
37
36
 
38
37
  Args:
38
+ ----
39
39
  file_path: Path of hdf5 file containing shepherd data with iv-samples, iv-curves or isc&voc
40
40
  verbose: more debug-info during usage, 'None' skips the setter
41
+
41
42
  """
42
43
 
43
44
  samples_per_buffer: int = 10_000
@@ -158,7 +159,7 @@ class Reader:
158
159
  )
159
160
 
160
161
  def _refresh_file_stats(self) -> None:
161
- """update internal states, helpful after resampling or other changes in data-group"""
162
+ """Update internal states, helpful after resampling or other changes in data-group"""
162
163
  self.h5file.flush()
163
164
  if (self.ds_time.shape[0] > 1) and (self.ds_time[1] != self.ds_time[0]):
164
165
  # this assumes isochronal sampling
@@ -182,15 +183,17 @@ class Reader:
182
183
  ) -> Generator[tuple, None, None]:
183
184
  """Generator that reads the specified range of buffers from the hdf5 file.
184
185
  can be configured on first call
185
- TODO: reconstruct - start/end mark samples and
186
- each call can request a certain number of samples
186
+ TODO: reconstruct - start/end mark samples &
187
+ each call can request a certain number of samples
187
188
 
188
189
  Args:
190
+ ----
189
191
  :param start_n: (int) Index of first buffer to be read
190
192
  :param end_n: (int) Index of last buffer to be read
191
193
  :param is_raw: (bool) output original data, not transformed to SI-Units
192
194
  :param omit_ts: (bool) optimize reading if timestamp is never used
193
195
  Yields: Buffers between start and end (tuple with time, voltage, current)
196
+
194
197
  """
195
198
  if end_n is None:
196
199
  end_n = int(self.ds_voltage.shape[0] // self.samples_per_buffer)
@@ -250,7 +253,7 @@ class Reader:
250
253
  return None
251
254
 
252
255
  def get_hrv_config(self) -> dict:
253
- """essential info for harvester
256
+ """Essential info for harvester
254
257
  :return: config-dict directly for vHarvester to be used during emulation
255
258
  """
256
259
  return {
@@ -259,7 +262,7 @@ class Reader:
259
262
  }
260
263
 
261
264
  def is_valid(self) -> bool:
262
- """checks file for plausibility
265
+ """Checks file for plausibility
263
266
 
264
267
  :return: state of validity
265
268
  """
@@ -387,7 +390,7 @@ class Reader:
387
390
  return True
388
391
 
389
392
  def __getitem__(self, key: str) -> Any:
390
- """returns attribute or (if none found) a handle for a group or dataset (if found)
393
+ """Returns attribute or (if none found) a handle for a group or dataset (if found)
391
394
 
392
395
  :param key: attribute, group, dataset
393
396
  :return: value of that key, or handle of object
@@ -399,7 +402,7 @@ class Reader:
399
402
  raise KeyError
400
403
 
401
404
  def energy(self) -> float:
402
- """determine the recorded energy of the trace
405
+ """Determine the recorded energy of the trace
403
406
  # multiprocessing: https://stackoverflow.com/a/71898911
404
407
  # -> failed with multiprocessing.pool and pathos.multiprocessing.ProcessPool
405
408
 
@@ -427,7 +430,7 @@ class Reader:
427
430
  def _dset_statistics(
428
431
  self, dset: h5py.Dataset, cal: Optional[CalibrationPair] = None
429
432
  ) -> Dict[str, float]:
430
- """some basic stats for a provided dataset
433
+ """Some basic stats for a provided dataset
431
434
  :param dset: dataset to evaluate
432
435
  :param cal: calibration (if wanted)
433
436
  :return: dict with entries for mean, min, max, std
@@ -469,7 +472,7 @@ class Reader:
469
472
  return stats
470
473
 
471
474
  def _data_timediffs(self) -> List[float]:
472
- """calculate list of (unique) time-deltas between buffers [s]
475
+ """Calculate list of (unique) time-deltas between buffers [s]
473
476
  -> optimized version that only looks at the start of each buffer
474
477
 
475
478
  :return: list of (unique) time-deltas between buffers [s]
@@ -500,7 +503,7 @@ class Reader:
500
503
  return list(diffs)
501
504
 
502
505
  def check_timediffs(self) -> bool:
503
- """validate equal time-deltas
506
+ """Validate equal time-deltas
504
507
  -> unexpected time-jumps hint at a corrupted file or faulty measurement
505
508
 
506
509
  :return: True if OK
@@ -529,7 +532,7 @@ class Reader:
529
532
  *,
530
533
  minimal: bool = False,
531
534
  ) -> Dict[str, dict]:
532
- """recursive FN to capture the structure of the file
535
+ """Recursive FN to capture the structure of the file
533
536
  :param node: starting node, leave free to go through whole file
534
537
  :param minimal: just provide a bare tree (much faster)
535
538
  :return: structure of that node with everything inside it
@@ -580,7 +583,7 @@ class Reader:
580
583
  return metadata
581
584
 
582
585
  def save_metadata(self, node: Union[h5py.Dataset, h5py.Group, None] = None) -> dict:
583
- """get structure of file and dump content to yaml-file with same name as original
586
+ """Get structure of file and dump content to yaml-file with same name as original
584
587
 
585
588
  :param node: starting node, leave free to go through whole file
586
589
  :return: structure of that node with everything inside it
@@ -609,7 +612,7 @@ class Reader:
609
612
 
610
613
  @staticmethod
611
614
  def get_filter_for_redundant_states(data: np.ndarray) -> np.ndarray:
612
- """input is 1D state-vector, kep only first from identical & sequential states
615
+ """Input is 1D state-vector, kep only first from identical & sequential states
613
616
  algo: create an offset-by-one vector and compare against original
614
617
  """
615
618
  if len(data.shape) > 1:
@@ -0,0 +1,15 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+
5
+ def _get_xdg_path(variable_name: str, default_path: Path) -> Path:
6
+ _value = os.environ.get(variable_name)
7
+ if _value is None or _value == "":
8
+ return default_path
9
+ return Path(_value)
10
+
11
+
12
+ user_path = Path("~").expanduser()
13
+
14
+ cache_xdg_path = _get_xdg_path("XDG_CACHE_HOME", user_path / ".cache")
15
+ cache_user_path = cache_xdg_path / "shepherd_datalib"