shepherd-data 2025.2.1__tar.gz → 2025.4.1__tar.gz

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 (28) hide show
  1. {shepherd_data-2025.2.1 → shepherd_data-2025.4.1}/PKG-INFO +3 -3
  2. {shepherd_data-2025.2.1 → shepherd_data-2025.4.1}/pyproject.toml +2 -1
  3. {shepherd_data-2025.2.1 → shepherd_data-2025.4.1}/shepherd_data/__init__.py +1 -1
  4. {shepherd_data-2025.2.1 → shepherd_data-2025.4.1}/shepherd_data/cli.py +65 -22
  5. {shepherd_data-2025.2.1 → shepherd_data-2025.4.1}/shepherd_data/mppt.py +5 -1
  6. {shepherd_data-2025.2.1 → shepherd_data-2025.4.1}/shepherd_data/reader.py +18 -14
  7. {shepherd_data-2025.2.1 → shepherd_data-2025.4.1}/shepherd_data.egg-info/PKG-INFO +3 -3
  8. {shepherd_data-2025.2.1 → shepherd_data-2025.4.1}/shepherd_data.egg-info/requires.txt +1 -1
  9. {shepherd_data-2025.2.1 → shepherd_data-2025.4.1}/README.md +0 -0
  10. {shepherd_data-2025.2.1 → shepherd_data-2025.4.1}/setup.cfg +0 -0
  11. {shepherd_data-2025.2.1 → shepherd_data-2025.4.1}/shepherd_data/ivonne.py +0 -0
  12. {shepherd_data-2025.2.1 → shepherd_data-2025.4.1}/shepherd_data.egg-info/SOURCES.txt +0 -0
  13. {shepherd_data-2025.2.1 → shepherd_data-2025.4.1}/shepherd_data.egg-info/dependency_links.txt +0 -0
  14. {shepherd_data-2025.2.1 → shepherd_data-2025.4.1}/shepherd_data.egg-info/entry_points.txt +0 -0
  15. {shepherd_data-2025.2.1 → shepherd_data-2025.4.1}/shepherd_data.egg-info/top_level.txt +0 -0
  16. {shepherd_data-2025.2.1 → shepherd_data-2025.4.1}/shepherd_data.egg-info/zip-safe +0 -0
  17. {shepherd_data-2025.2.1 → shepherd_data-2025.4.1}/tests/test_cli.py +0 -0
  18. {shepherd_data-2025.2.1 → shepherd_data-2025.4.1}/tests/test_cli_downsample.py +0 -0
  19. {shepherd_data-2025.2.1 → shepherd_data-2025.4.1}/tests/test_cli_extract.py +0 -0
  20. {shepherd_data-2025.2.1 → shepherd_data-2025.4.1}/tests/test_cli_extract_gpio.py +0 -0
  21. {shepherd_data-2025.2.1 → shepherd_data-2025.4.1}/tests/test_cli_extract_meta.py +0 -0
  22. {shepherd_data-2025.2.1 → shepherd_data-2025.4.1}/tests/test_cli_extract_uart.py +0 -0
  23. {shepherd_data-2025.2.1 → shepherd_data-2025.4.1}/tests/test_cli_plot.py +0 -0
  24. {shepherd_data-2025.2.1 → shepherd_data-2025.4.1}/tests/test_cli_validate.py +0 -0
  25. {shepherd_data-2025.2.1 → shepherd_data-2025.4.1}/tests/test_cli_version.py +0 -0
  26. {shepherd_data-2025.2.1 → shepherd_data-2025.4.1}/tests/test_examples.py +0 -0
  27. {shepherd_data-2025.2.1 → shepherd_data-2025.4.1}/tests/test_ivonne.py +0 -0
  28. {shepherd_data-2025.2.1 → shepherd_data-2025.4.1}/tests/test_reader.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: shepherd_data
3
- Version: 2025.2.1
3
+ Version: 2025.4.1
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>
@@ -36,7 +36,7 @@ Requires-Dist: numpy
36
36
  Requires-Dist: pandas>=2.0.0
37
37
  Requires-Dist: pyYAML
38
38
  Requires-Dist: scipy
39
- Requires-Dist: shepherd-core[inventory]>=2025.02.1
39
+ Requires-Dist: shepherd-core[inventory]>=2025.04.1
40
40
  Requires-Dist: tqdm
41
41
  Provides-Extra: elf
42
42
  Requires-Dist: shepherd-core[elf]; extra == "elf"
@@ -38,7 +38,7 @@ dependencies = [
38
38
  "pandas>=2.0.0", # full-version, v2 is OK
39
39
  "pyYAML",
40
40
  "scipy", # full-version
41
- "shepherd-core[inventory]>=2025.02.1", # libs are strongly coupled
41
+ "shepherd-core[inventory]>=2025.04.1", # libs are strongly coupled
42
42
  "tqdm", # full-version
43
43
  ]
44
44
 
@@ -103,3 +103,4 @@ source = ["shepherd_data"]
103
103
  [tool.mypy]
104
104
  python_version = 3.8
105
105
  ignore_missing_imports = true
106
+ disable_error_code = ["call-arg", ]
@@ -11,7 +11,7 @@ from shepherd_core import Writer
11
11
 
12
12
  from .reader import Reader
13
13
 
14
- __version__ = "2025.02.1"
14
+ __version__ = "2025.04.1"
15
15
 
16
16
  __all__ = [
17
17
  "Reader",
@@ -1,7 +1,6 @@
1
1
  """Command definitions for CLI."""
2
2
 
3
3
  import logging
4
- import os
5
4
  import sys
6
5
  from contextlib import suppress
7
6
  from datetime import datetime
@@ -22,7 +21,7 @@ from .reader import Reader
22
21
  logger = logging.getLogger("SHPData.cli")
23
22
 
24
23
 
25
- def path_to_flist(data_path: Path) -> List[Path]:
24
+ def path_to_flist(data_path: Path, *, recurse: bool = False) -> List[Path]:
26
25
  """Every path gets transformed to a list of paths.
27
26
 
28
27
  Transformations:
@@ -31,16 +30,14 @@ def path_to_flist(data_path: Path) -> List[Path]:
31
30
  - or else: empty list
32
31
  """
33
32
  data_path = Path(data_path).resolve()
34
- h5files = []
33
+ h5files: list = []
35
34
  if data_path.is_file() and data_path.suffix.lower() == ".h5":
36
35
  h5files.append(data_path)
37
36
  elif data_path.is_dir():
38
- flist = os.listdir(data_path)
39
- for file in flist:
40
- fpath = data_path / str(file)
41
- if not fpath.is_file() or fpath.suffix.lower() != ".h5":
42
- continue
43
- h5files.append(fpath)
37
+ files = data_path.glob(
38
+ "**/*.h5" if recurse else "*.h5"
39
+ ) # for py>=3.12: case_sensitive=False
40
+ h5files = [file for file in files if file.is_file()]
44
41
  return h5files
45
42
 
46
43
 
@@ -70,9 +67,15 @@ def version() -> None:
70
67
 
71
68
  @cli.command(short_help="Validates a file or directory containing shepherd-recordings")
72
69
  @click.argument("in_data", type=click.Path(exists=True, resolve_path=True))
73
- def validate(in_data: Path) -> None:
70
+ @click.option(
71
+ "--recurse",
72
+ "-a",
73
+ is_flag=True,
74
+ help="Also consider files in sub-folders",
75
+ )
76
+ def validate(in_data: Path, *, recurse: bool = False) -> None:
74
77
  """Validate a file or directory containing shepherd-recordings."""
75
- files = path_to_flist(in_data)
78
+ files = path_to_flist(in_data, recurse=recurse)
76
79
  verbose_level = get_verbose_level() # TODO: should be stored and passed in ctx
77
80
  valid_dir = True
78
81
  for file in files:
@@ -123,7 +126,13 @@ def validate(in_data: Path) -> None:
123
126
  "--raw",
124
127
  "-r",
125
128
  is_flag=True,
126
- help="Plot only power instead of voltage, current & power",
129
+ help="Don't convert data to si-units",
130
+ )
131
+ @click.option(
132
+ "--recurse",
133
+ "-a",
134
+ is_flag=True,
135
+ help="Also consider files in sub-folders",
127
136
  )
128
137
  def extract(
129
138
  in_data: Path,
@@ -133,9 +142,10 @@ def extract(
133
142
  separator: str,
134
143
  *,
135
144
  raw: bool = False,
145
+ recurse: bool = False,
136
146
  ) -> None:
137
147
  """Extract recorded IVSamples and store them to csv."""
138
- files = path_to_flist(in_data)
148
+ files = path_to_flist(in_data, recurse=recurse)
139
149
  verbose_level = get_verbose_level()
140
150
  if not isinstance(ds_factor, (float, int)) or ds_factor < 1:
141
151
  ds_factor = 1000
@@ -162,10 +172,15 @@ def extract(
162
172
  type=click.STRING,
163
173
  help="Set an individual csv-separator",
164
174
  )
165
- # TODO: a recursive option would help!
166
- def extract_meta(in_data: Path, separator: str) -> None:
175
+ @click.option(
176
+ "--recurse",
177
+ "-a",
178
+ is_flag=True,
179
+ help="Also consider files in sub-folders",
180
+ )
181
+ def extract_meta(in_data: Path, separator: str, *, recurse: bool = False) -> None:
167
182
  """Extract metadata and logs from file or directory containing shepherd-recordings."""
168
- files = path_to_flist(in_data)
183
+ files = path_to_flist(in_data, recurse=recurse)
169
184
  verbose_level = get_verbose_level()
170
185
  for file in files:
171
186
  logger.info("Extracting metadata & logs from '%s' ...", file.name)
@@ -195,9 +210,15 @@ def extract_meta(in_data: Path, separator: str) -> None:
195
210
  short_help="Extracts uart from gpio-trace in file or directory containing shepherd-recordings"
196
211
  )
197
212
  @click.argument("in_data", type=click.Path(exists=True, resolve_path=True))
198
- def extract_uart(in_data: Path) -> None:
213
+ @click.option(
214
+ "--recurse",
215
+ "-a",
216
+ is_flag=True,
217
+ help="Also consider files in sub-folders",
218
+ )
219
+ def extract_uart(in_data: Path, *, recurse: bool = False) -> None:
199
220
  """Extract UART from GPIO-trace in file or directory containing shepherd-recordings."""
200
- files = path_to_flist(in_data)
221
+ files = path_to_flist(in_data, recurse=recurse)
201
222
  verbose_level = get_verbose_level()
202
223
  for file in files:
203
224
  logger.info("Extracting uart from gpio-trace from from '%s' ...", file.name)
@@ -220,6 +241,7 @@ def extract_uart(in_data: Path) -> None:
220
241
  log_file.write(timestamp.strftime("%Y-%m-%d %H:%M:%S.%f") + ":")
221
242
  # TODO: allow to skip Timestamp and export raw text
222
243
  log_file.write(f"\t{str.encode(line[1])}")
244
+ # TODO: does this produce "\tb'abc'"?
223
245
  log_file.write("\n")
224
246
  except TypeError:
225
247
  logger.exception("ERROR: Will skip file. It caused an exception.")
@@ -233,10 +255,16 @@ def extract_uart(in_data: Path) -> None:
233
255
  default=";",
234
256
  type=click.STRING,
235
257
  help="Set an individual csv-separator",
258
+ ) # TODO: also configure decimal point
259
+ @click.option(
260
+ "--recurse",
261
+ "-a",
262
+ is_flag=True,
263
+ help="Also consider files in sub-folders",
236
264
  )
237
- def extract_gpio(in_data: Path, separator: str) -> None:
265
+ def extract_gpio(in_data: Path, separator: str, *, recurse: bool = False) -> None:
238
266
  """Extract UART from gpio-trace in file or directory containing shepherd-recordings."""
239
- files = path_to_flist(in_data)
267
+ files = path_to_flist(in_data, recurse=recurse)
240
268
  verbose_level = get_verbose_level()
241
269
  for file in files:
242
270
  logger.info("Extracting gpio-trace from from '%s' ...", file.name)
@@ -281,15 +309,23 @@ def extract_gpio(in_data: Path, separator: str) -> None:
281
309
  type=click.FLOAT,
282
310
  help="End-point in seconds, will be max if omitted",
283
311
  )
312
+ @click.option(
313
+ "--recurse",
314
+ "-a",
315
+ is_flag=True,
316
+ help="Also consider files in sub-folders",
317
+ )
284
318
  def downsample(
285
319
  in_data: Path,
286
320
  ds_factor: Optional[float],
287
321
  sample_rate: Optional[int],
288
322
  start: Optional[float],
289
323
  end: Optional[float],
324
+ *,
325
+ recurse: bool = False,
290
326
  ) -> None:
291
327
  """Create an array of down-sampled files from file or dir containing shepherd-recordings."""
292
- files = path_to_flist(in_data)
328
+ files = path_to_flist(in_data, recurse=recurse)
293
329
  verbose_level = get_verbose_level()
294
330
  for file in files:
295
331
  try:
@@ -351,6 +387,12 @@ def downsample(
351
387
  is_flag=True,
352
388
  help="Plot only power instead of voltage, current & power",
353
389
  )
390
+ @click.option(
391
+ "--recurse",
392
+ "-a",
393
+ is_flag=True,
394
+ help="Also consider files in sub-folders",
395
+ )
354
396
  def plot(
355
397
  in_data: Path,
356
398
  start: Optional[float],
@@ -360,9 +402,10 @@ def plot(
360
402
  *,
361
403
  multiplot: bool,
362
404
  only_power: bool,
405
+ recurse: bool = False,
363
406
  ) -> None:
364
407
  """Plot IV-trace from file or directory containing shepherd-recordings."""
365
- files = path_to_flist(in_data)
408
+ files = path_to_flist(in_data, recurse=recurse)
366
409
  verbose_level = get_verbose_level()
367
410
  multiplot = multiplot and len(files) > 1
368
411
  data = []
@@ -3,6 +3,9 @@
3
3
  Might be exchanged by shepherds py-model of pru-harvesters.
4
4
  """
5
5
 
6
+ from abc import ABC
7
+ from abc import abstractmethod
8
+
6
9
  import numpy as np
7
10
  import pandas as pd
8
11
 
@@ -36,7 +39,7 @@ def find_oc(v_arr: np.ndarray, i_arr: np.ndarray, ratio: float = 0.05) -> np.nda
36
39
  return v_arr[np.argmax(i_arr < i_arr[0] * ratio)]
37
40
 
38
41
 
39
- class MPPTracker:
42
+ class MPPTracker(ABC):
40
43
  """Prototype for a MPPT-class.
41
44
 
42
45
  :param v_max: Maximum voltage supported by shepherd
@@ -48,6 +51,7 @@ class MPPTracker:
48
51
  self.v_max: float = v_max
49
52
  self.v_proto: np.ndarray = np.linspace(0, v_max, pts_per_curve)
50
53
 
54
+ @abstractmethod
51
55
  def process(self, coeffs: pd.DataFrame) -> pd.DataFrame:
52
56
  """Apply harvesting model to input data.
53
57
 
@@ -4,7 +4,10 @@ import math
4
4
  from datetime import datetime
5
5
  from pathlib import Path
6
6
  from typing import Dict
7
+ from typing import List
8
+ from typing import Mapping
7
9
  from typing import Optional
10
+ from typing import Sequence
8
11
  from typing import Union
9
12
 
10
13
  import h5py
@@ -56,21 +59,25 @@ class Reader(CoreReader):
56
59
  if csv_path.exists():
57
60
  self._logger.info("File already exists, will skip '%s'", csv_path.name)
58
61
  return 0
59
- datasets = [key if isinstance(h5_group[key], h5py.Dataset) else [] for key in h5_group]
62
+ datasets: List[str] = [
63
+ str(key) for key in h5_group if isinstance(h5_group[key], h5py.Dataset)
64
+ ]
60
65
  datasets.remove("time")
61
66
  datasets = ["time", *datasets]
62
67
  separator = separator.strip().ljust(2)
63
- header = [h5_group[key].attrs["description"].replace(", ", separator) for key in datasets]
64
- header = separator.join(header)
68
+ header_elements: List[str] = [
69
+ str(h5_group[key].attrs["description"]).replace(", ", separator) for key in datasets
70
+ ]
71
+ header: str = separator.join(header_elements)
65
72
  with csv_path.open("w", encoding="utf-8-sig") as csv_file:
66
73
  self._logger.info("CSV-Generator will save '%s' to '%s'", h5_group.name, csv_path.name)
67
74
  csv_file.write(header + "\n")
68
75
  ts_gain = h5_group["time"].attrs.get("gain", 1e-9)
69
76
  # for converting data to si - if raw=false
70
- gains: dict[str, float] = {
77
+ gains: Dict[str, float] = {
71
78
  key: h5_group[key].attrs.get("gain", 1.0) for key in datasets[1:]
72
79
  }
73
- offsets: dict[str, float] = {
80
+ offsets: Dict[str, float] = {
74
81
  key: h5_group[key].attrs.get("offset", 1.0) for key in datasets[1:]
75
82
  }
76
83
  for idx, time_ns in enumerate(h5_group["time"][:]):
@@ -222,7 +229,7 @@ class Reader(CoreReader):
222
229
  for _iter in trange(
223
230
  0,
224
231
  iterations,
225
- desc=f"downsampling {data_src.name}",
232
+ desc=f"downsampling {data_src.name if isinstance(data_src, h5py.Dataset) else ''}",
226
233
  leave=False,
227
234
  disable=iterations < 8,
228
235
  ):
@@ -242,7 +249,7 @@ class Reader(CoreReader):
242
249
  self,
243
250
  start_s: Optional[float],
244
251
  end_s: Optional[float],
245
- ds_factor: Optional[float],
252
+ ds_factor: float,
246
253
  ) -> Path:
247
254
  """Cut source to given limits, downsample by factor and store result in separate file.
248
255
 
@@ -290,10 +297,7 @@ class Reader(CoreReader):
290
297
  else:
291
298
  cut_str = ""
292
299
 
293
- if ds_factor > 1: # noqa: SIM108
294
- ds_str = f".downsample_x{round(ds_factor)}"
295
- else:
296
- ds_str = ""
300
+ ds_str = f".downsample_x{round(ds_factor)}" if ds_factor > 1 else ""
297
301
 
298
302
  dst_file = self.file_path.resolve().with_suffix(cut_str + ds_str + ".h5")
299
303
  if dst_file.exists():
@@ -403,7 +407,7 @@ class Reader(CoreReader):
403
407
  for _ in trange(
404
408
  0,
405
409
  iterations,
406
- desc=f"resampling {data_src.name}",
410
+ desc=f"resampling {data_src.name if isinstance(data_src, h5py.Dataset) else ''}",
407
411
  leave=False,
408
412
  disable=iterations < 8,
409
413
  ):
@@ -506,7 +510,7 @@ class Reader(CoreReader):
506
510
 
507
511
  @staticmethod
508
512
  def assemble_plot(
509
- data: Union[dict, list], width: int = 20, height: int = 10, *, only_pwr: bool = False
513
+ data: Union[Mapping, Sequence], width: int = 20, height: int = 10, *, only_pwr: bool = False
510
514
  ) -> plt.Figure:
511
515
  """Create the actual figure.
512
516
 
@@ -520,7 +524,7 @@ class Reader(CoreReader):
520
524
  :return: figure
521
525
  """
522
526
  # TODO: allow choosing freely from I, V, P, GPIO
523
- if isinstance(data, dict):
527
+ if isinstance(data, Mapping):
524
528
  data = [data]
525
529
  if only_pwr:
526
530
  fig, ax = plt.subplots(1, 1, figsize=(width, height), layout="tight")
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: shepherd_data
3
- Version: 2025.2.1
3
+ Version: 2025.4.1
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>
@@ -36,7 +36,7 @@ Requires-Dist: numpy
36
36
  Requires-Dist: pandas>=2.0.0
37
37
  Requires-Dist: pyYAML
38
38
  Requires-Dist: scipy
39
- Requires-Dist: shepherd-core[inventory]>=2025.02.1
39
+ Requires-Dist: shepherd-core[inventory]>=2025.04.1
40
40
  Requires-Dist: tqdm
41
41
  Provides-Extra: elf
42
42
  Requires-Dist: shepherd-core[elf]; extra == "elf"
@@ -5,7 +5,7 @@ numpy
5
5
  pandas>=2.0.0
6
6
  pyYAML
7
7
  scipy
8
- shepherd-core[inventory]>=2025.02.1
8
+ shepherd-core[inventory]>=2025.04.1
9
9
  tqdm
10
10
 
11
11
  [dev]