shepherd-data 2024.9.1__py3-none-any.whl → 2024.11.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
shepherd_data/__init__.py CHANGED
@@ -11,7 +11,7 @@ from shepherd_core import Writer
11
11
 
12
12
  from .reader import Reader
13
13
 
14
- __version__ = "2024.9.1"
14
+ __version__ = "2024.11.2"
15
15
 
16
16
  __all__ = [
17
17
  "Reader",
shepherd_data/cli.py CHANGED
@@ -10,13 +10,12 @@ from typing import List
10
10
  from typing import Optional
11
11
 
12
12
  import click
13
+ import pydantic
13
14
 
14
15
  from shepherd_core import get_verbose_level
15
- from shepherd_core import increase_verbose_level
16
16
  from shepherd_core import local_tz
17
- from shepherd_core.commons import samplerate_sps_default
17
+ from shepherd_core.logger import set_log_verbose_level
18
18
 
19
- from . import Writer
20
19
  from . import __version__
21
20
  from .reader import Reader
22
21
 
@@ -50,26 +49,25 @@ def path_to_flist(data_path: Path) -> List[Path]:
50
49
  "--verbose",
51
50
  "-v",
52
51
  is_flag=True,
53
- help="4 Levels [0..3](Error, Warning, Info, Debug)",
54
- )
55
- @click.option(
56
- "--version",
57
- is_flag=True,
58
- help="Prints version-info at start (combinable with -v)",
52
+ help="Switch from info- to debug-level",
59
53
  )
60
54
  @click.pass_context # TODO: is the ctx-type correct?
61
- def cli(ctx: click.Context, *, verbose: bool, version: bool) -> None:
55
+ def cli(ctx: click.Context, *, verbose: bool) -> None:
62
56
  """Shepherd: Synchronized Energy Harvesting Emulator and Recorder."""
63
- if verbose:
64
- increase_verbose_level(3)
65
- if version:
66
- logger.info("Shepherd-Data v%s", __version__)
67
- logger.debug("Python v%s", sys.version)
68
- logger.debug("Click v%s", click.__version__)
57
+ set_log_verbose_level(logger, 3 if verbose else 2)
69
58
  if not ctx.invoked_subcommand:
70
59
  click.echo("Please specify a valid command")
71
60
 
72
61
 
62
+ @cli.command(short_help="Print version-info (combine with -v for more)")
63
+ def version() -> None:
64
+ """Print version-info (combine with -v for more)."""
65
+ logger.info("Shepherd-Data v%s", __version__)
66
+ logger.debug("Python v%s", sys.version)
67
+ logger.debug("Click v%s", click.__version__)
68
+ logger.debug("Pydantic v%s", pydantic.__version__)
69
+
70
+
73
71
  @cli.command(short_help="Validates a file or directory containing shepherd-recordings")
74
72
  @click.argument("in_data", type=click.Path(exists=True, resolve_path=True))
75
73
  def validate(in_data: Path) -> None:
@@ -94,6 +92,20 @@ def validate(in_data: Path) -> None:
94
92
 
95
93
  @cli.command(short_help="Extracts recorded IVSamples and stores it to csv")
96
94
  @click.argument("in_data", type=click.Path(exists=True, resolve_path=True))
95
+ @click.option(
96
+ "--start",
97
+ "-s",
98
+ default=None,
99
+ type=click.FLOAT,
100
+ help="Start-point in seconds, will be 0 if omitted",
101
+ )
102
+ @click.option(
103
+ "--end",
104
+ "-e",
105
+ default=None,
106
+ type=click.FLOAT,
107
+ help="End-point in seconds, will be max if omitted",
108
+ )
97
109
  @click.option(
98
110
  "--ds-factor",
99
111
  "-f",
@@ -103,12 +115,25 @@ def validate(in_data: Path) -> None:
103
115
  )
104
116
  @click.option(
105
117
  "--separator",
106
- "-s",
107
118
  default=";",
108
119
  type=click.STRING,
109
120
  help="Set an individual csv-separator",
110
121
  )
111
- def extract(in_data: Path, ds_factor: float, separator: str) -> None:
122
+ @click.option(
123
+ "--raw",
124
+ "-r",
125
+ is_flag=True,
126
+ help="Plot only power instead of voltage, current & power",
127
+ )
128
+ def extract(
129
+ in_data: Path,
130
+ start: Optional[float],
131
+ end: Optional[float],
132
+ ds_factor: float,
133
+ separator: str,
134
+ *,
135
+ raw: bool = False,
136
+ ) -> None:
112
137
  """Extract recorded IVSamples and store them to csv."""
113
138
  files = path_to_flist(in_data)
114
139
  verbose_level = get_verbose_level()
@@ -119,41 +144,10 @@ def extract(in_data: Path, ds_factor: float, separator: str) -> None:
119
144
  logger.info("Extracting IV-Samples from '%s' ...", file.name)
120
145
  try:
121
146
  with Reader(file, verbose=verbose_level > 2) as shpr:
122
- # TODO: this code is very similar to data.reader.downsample()
123
- if (shpr.ds_voltage.shape[0] / ds_factor) < 10:
124
- logger.warning(
125
- "will skip downsampling for %s because "
126
- "resulting sample-size is too small",
127
- file.name,
128
- )
129
- continue
130
- # will create a downsampled h5-file (if not existing) and then saving to csv
131
- ds_file = file.with_suffix(f".downsampled_x{round(ds_factor)}.h5")
132
- if not ds_file.exists():
133
- logger.info("Downsampling '%s' by factor x%f ...", file.name, ds_factor)
134
- with Writer(
135
- ds_file,
136
- mode=shpr.get_mode(),
137
- datatype=shpr.get_datatype(),
138
- window_samples=shpr.get_window_samples(),
139
- cal_data=shpr.get_calibration_data(),
140
- verbose=verbose_level > 2,
141
- ) as shpw:
142
- shpw["ds_factor"] = ds_factor
143
- shpw.store_hostname(shpr.get_hostname())
144
- shpw.store_config(shpr.get_config())
145
- shpr.downsample(
146
- shpr.ds_time,
147
- shpw.ds_time,
148
- ds_factor=ds_factor,
149
- is_time=True,
150
- )
151
- shpr.downsample(shpr.ds_voltage, shpw.ds_voltage, ds_factor=ds_factor)
152
- shpr.downsample(shpr.ds_current, shpw.ds_current, ds_factor=ds_factor)
153
-
154
- with Reader(ds_file, verbose=verbose_level > 2) as shpd:
155
- shpd.save_csv(shpd["data"], separator)
156
- except TypeError:
147
+ out_file = shpr.cut_and_downsample_to_file(start, end, ds_factor=ds_factor)
148
+ with Reader(out_file, verbose=verbose_level > 2) as shpd:
149
+ shpd.save_csv(shpd["data"], separator, raw=raw)
150
+ except (TypeError, ValueError):
157
151
  logger.exception("ERROR: Will skip file. It caused an exception.")
158
152
 
159
153
 
@@ -256,11 +250,10 @@ def extract_gpio(in_data: Path, separator: str) -> None:
256
250
 
257
251
 
258
252
  @cli.command(
259
- short_help="Creates an array of downsampling-files from "
253
+ short_help="Creates an array of down-sampled files from "
260
254
  "file or directory containing shepherd-recordings"
261
255
  )
262
256
  @click.argument("in_data", type=click.Path(exists=True, resolve_path=True))
263
- # @click.option("--out_data", "-o", type=click.Path(resolve_path=True))
264
257
  @click.option(
265
258
  "--ds-factor",
266
259
  "-f",
@@ -274,48 +267,45 @@ def extract_gpio(in_data: Path, separator: str) -> None:
274
267
  type=click.INT,
275
268
  help="Alternative Input to determine a downsample-factor (Choose One)",
276
269
  )
277
- def downsample(in_data: Path, ds_factor: Optional[float], sample_rate: Optional[int]) -> None:
270
+ @click.option(
271
+ "--start",
272
+ "-s",
273
+ default=None,
274
+ type=click.FLOAT,
275
+ help="Start-point in seconds, will be 0 if omitted",
276
+ )
277
+ @click.option(
278
+ "--end",
279
+ "-e",
280
+ default=None,
281
+ type=click.FLOAT,
282
+ help="End-point in seconds, will be max if omitted",
283
+ )
284
+ def downsample(
285
+ in_data: Path,
286
+ ds_factor: Optional[float],
287
+ sample_rate: Optional[int],
288
+ start: Optional[float],
289
+ end: Optional[float],
290
+ ) -> None:
278
291
  """Create an array of down-sampled files from file or dir containing shepherd-recordings."""
279
- if ds_factor is None and sample_rate is not None and sample_rate >= 1:
280
- ds_factor = int(samplerate_sps_default / sample_rate)
281
- # TODO: shouldn't current sps be based on file rather than default?
282
- if isinstance(ds_factor, (float, int)) and ds_factor >= 1:
283
- ds_list = [ds_factor]
284
- else:
285
- ds_list = [5, 25, 100, 500, 2_500, 10_000, 50_000, 250_000, 1_000_000]
286
-
287
292
  files = path_to_flist(in_data)
288
293
  verbose_level = get_verbose_level()
289
294
  for file in files:
290
295
  try:
291
296
  with Reader(file, verbose=verbose_level > 2) as shpr:
297
+ if ds_factor is None and sample_rate is not None and sample_rate >= 1:
298
+ ds_factor = shpr.samplerate_sps / sample_rate
299
+
300
+ if isinstance(ds_factor, (float, int)) and ds_factor >= 1:
301
+ ds_list = [ds_factor]
302
+ else:
303
+ ds_list = [5, 25, 100, 500, 2_500, 10_000, 50_000, 250_000, 1_000_000]
304
+
292
305
  for _factor in ds_list:
293
- if (shpr.ds_voltage.shape[0] / _factor) < 1000:
294
- logger.warning(
295
- "will skip downsampling for %s because "
296
- "resulting sample-size is too small",
297
- file.name,
298
- )
299
- break
300
- ds_file = file.with_suffix(f".downsampled_x{round(_factor)}.h5")
301
- if ds_file.exists():
302
- continue
303
- logger.info("Downsampling '%s' by factor x%f ...", file.name, _factor)
304
- with Writer(
305
- ds_file,
306
- mode=shpr.get_mode(),
307
- datatype=shpr.get_datatype(),
308
- window_samples=shpr.get_window_samples(),
309
- cal_data=shpr.get_calibration_data(),
310
- verbose=verbose_level > 2,
311
- ) as shpw:
312
- shpw["ds_factor"] = _factor
313
- shpw.store_hostname(shpr.get_hostname())
314
- shpw.store_config(shpr.get_config())
315
- shpr.downsample(shpr.ds_time, shpw.ds_time, ds_factor=_factor, is_time=True)
316
- shpr.downsample(shpr.ds_voltage, shpw.ds_voltage, ds_factor=_factor)
317
- shpr.downsample(shpr.ds_current, shpw.ds_current, ds_factor=_factor)
318
- except TypeError:
306
+ path_file = shpr.cut_and_downsample_to_file(start, end, _factor)
307
+ logger.info("Created %s", path_file.name)
308
+ except (TypeError, ValueError): # noqa: PERF203
319
309
  logger.exception("ERROR: Will skip file. It caused an exception.")
320
310
 
321
311
 
@@ -326,14 +316,14 @@ def downsample(in_data: Path, ds_factor: Optional[float], sample_rate: Optional[
326
316
  "-s",
327
317
  default=None,
328
318
  type=click.FLOAT,
329
- help="Start of plot in seconds, will be 0 if omitted",
319
+ help="Start-point in seconds, will be 0 if omitted",
330
320
  )
331
321
  @click.option(
332
322
  "--end",
333
323
  "-e",
334
324
  default=None,
335
325
  type=click.FLOAT,
336
- help="End of plot in seconds, will be max if omitted",
326
+ help="End-point in seconds, will be max if omitted",
337
327
  )
338
328
  @click.option(
339
329
  "--width",
shepherd_data/reader.py CHANGED
@@ -13,7 +13,9 @@ from matplotlib import pyplot as plt
13
13
  from tqdm import trange
14
14
 
15
15
  from shepherd_core import Reader as CoreReader
16
+ from shepherd_core import Writer as CoreWriter
16
17
  from shepherd_core import local_tz
18
+ from shepherd_core.logger import get_verbose_level
17
19
  from shepherd_core.logger import logger
18
20
 
19
21
  # import samplerate # noqa: ERA001, TODO: just a test-fn for now
@@ -37,11 +39,12 @@ class Reader(CoreReader):
37
39
  ) -> None:
38
40
  super().__init__(file_path, verbose=verbose)
39
41
 
40
- def save_csv(self, h5_group: h5py.Group, separator: str = ";") -> int:
42
+ def save_csv(self, h5_group: h5py.Group, separator: str = ";", *, raw: bool = False) -> int:
41
43
  """Extract numerical data from group and store it into csv.
42
44
 
43
45
  :param h5_group: can be external and should probably be downsampled
44
46
  :param separator: used between columns
47
+ :param raw: don't convert to si-units
45
48
  :return: number of processed entries
46
49
  """
47
50
  if ("time" not in h5_group) or (h5_group["time"].shape[0] < 1):
@@ -62,11 +65,21 @@ class Reader(CoreReader):
62
65
  with csv_path.open("w", encoding="utf-8-sig") as csv_file:
63
66
  self._logger.info("CSV-Generator will save '%s' to '%s'", h5_group.name, csv_path.name)
64
67
  csv_file.write(header + "\n")
68
+ ts_gain = h5_group["time"].attrs.get("gain", 1e-9)
69
+ # for converting data to si - if raw=false
70
+ gains: dict[str, float] = {
71
+ key: h5_group[key].attrs.get("gain", 1.0) for key in datasets[1:]
72
+ }
73
+ offsets: dict[str, float] = {
74
+ key: h5_group[key].attrs.get("offset", 1.0) for key in datasets[1:]
75
+ }
65
76
  for idx, time_ns in enumerate(h5_group["time"][:]):
66
- timestamp = datetime.fromtimestamp(time_ns / 1e9, tz=local_tz())
77
+ timestamp = datetime.fromtimestamp(time_ns * ts_gain, tz=local_tz())
67
78
  csv_file.write(timestamp.strftime("%Y-%m-%d %H:%M:%S.%f"))
68
79
  for key in datasets[1:]:
69
80
  values = h5_group[key][idx]
81
+ if not raw:
82
+ values = values * gains[key] + offsets[key]
70
83
  if isinstance(values, np.ndarray):
71
84
  values = separator.join([str(value) for value in values])
72
85
  csv_file.write(f"{separator}{values}")
@@ -225,6 +238,115 @@ class Reader(CoreReader):
225
238
  data_dst.resize((oblock_len * (iterations - 1) + slice_len,))
226
239
  return data_dst
227
240
 
241
+ def cut_and_downsample_to_file(
242
+ self,
243
+ start_s: Optional[float],
244
+ end_s: Optional[float],
245
+ ds_factor: Optional[float],
246
+ ) -> Path:
247
+ """Cut source to given limits, downsample by factor and store result in separate file.
248
+
249
+ Resulting file-name is derived from input-name by adding
250
+ - ".cut_x_to_y" and
251
+ - ".downsample_x"
252
+ when applicable.
253
+ """
254
+ # prepare cut
255
+ if not isinstance(start_s, (float, int)):
256
+ start_s = 0
257
+ if not isinstance(end_s, (float, int)):
258
+ end_s = self.runtime_s
259
+ start_s = max(0, start_s)
260
+ end_s = min(self.runtime_s, end_s)
261
+
262
+ start_sample = round(start_s * self.samplerate_sps)
263
+ end_sample = round(end_s * self.samplerate_sps)
264
+
265
+ # test input-parameters
266
+ if end_sample < start_sample:
267
+ raise ValueError(
268
+ "Cut & downsample for %s failed because "
269
+ "end-mark (%.3f) is before start-mark (%.3f).",
270
+ self.file_path.name,
271
+ end_s,
272
+ start_s,
273
+ )
274
+ if ds_factor < 1:
275
+ raise ValueError(
276
+ "Cut & downsample for %s failed because factor < 1",
277
+ self.file_path.name,
278
+ )
279
+ if ((end_sample - start_sample) / ds_factor) < 1000:
280
+ raise ValueError(
281
+ "Cut & downsample for %s failed because " "resulting sample-size is too small",
282
+ self.file_path.name,
283
+ )
284
+
285
+ # assemble file-name of output
286
+ if start_s != 0.0 or end_s != self.runtime_s:
287
+ start_str = f"{start_s:.3f}".replace(".", "s")
288
+ end_str = f"{end_s:.3f}".replace(".", "s")
289
+ cut_str = f".cut_{start_str}_to_{end_str}"
290
+ else:
291
+ cut_str = ""
292
+
293
+ if ds_factor > 1: # noqa: SIM108
294
+ ds_str = f".downsample_x{round(ds_factor)}"
295
+ else:
296
+ ds_str = ""
297
+
298
+ dst_file = self.file_path.resolve().with_suffix(cut_str + ds_str + ".h5")
299
+ if dst_file.exists():
300
+ logger.warning(
301
+ "Cut & Downsample skipped because output-file %s already exists.", dst_file.name
302
+ )
303
+ return dst_file
304
+
305
+ logger.debug(
306
+ "Cut & Downsample '%s' from %.3f s to %.3f s with factor = %.1f ...",
307
+ self.file_path.name,
308
+ start_s,
309
+ end_s,
310
+ ds_factor,
311
+ )
312
+
313
+ # convert data
314
+ with CoreWriter(
315
+ dst_file,
316
+ mode=self.get_mode(),
317
+ datatype=self.get_datatype(),
318
+ window_samples=self.get_window_samples(),
319
+ cal_data=self.get_calibration_data(),
320
+ verbose=get_verbose_level() > 2,
321
+ ) as shpw:
322
+ shpw["ds_factor"] = ds_factor
323
+ shpw.store_hostname(self.get_hostname())
324
+ shpw.store_config(self.get_config())
325
+ self.downsample(
326
+ self.ds_time,
327
+ shpw.ds_time,
328
+ start_n=start_sample,
329
+ end_n=end_sample,
330
+ ds_factor=ds_factor,
331
+ is_time=True,
332
+ )
333
+ self.downsample(
334
+ self.ds_voltage,
335
+ shpw.ds_voltage,
336
+ start_n=start_sample,
337
+ end_n=end_sample,
338
+ ds_factor=ds_factor,
339
+ )
340
+ self.downsample(
341
+ self.ds_current,
342
+ shpw.ds_current,
343
+ start_n=start_sample,
344
+ end_n=end_sample,
345
+ ds_factor=ds_factor,
346
+ )
347
+
348
+ return dst_file
349
+
228
350
  def resample(
229
351
  self,
230
352
  data_src: Union[h5py.Dataset, np.ndarray],
@@ -348,6 +470,8 @@ class Reader(CoreReader):
348
470
  start_s = 0
349
471
  if not isinstance(end_s, (float, int)):
350
472
  end_s = self.runtime_s
473
+ start_s = max(0, start_s)
474
+ end_s = min(self.runtime_s, end_s)
351
475
  start_sample = round(start_s * self.samplerate_sps)
352
476
  end_sample = round(end_s * self.samplerate_sps)
353
477
  if end_sample - start_sample < 5:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: shepherd_data
3
- Version: 2024.9.1
3
+ Version: 2024.11.2
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>
@@ -33,21 +33,21 @@ Requires-Dist: click
33
33
  Requires-Dist: h5py
34
34
  Requires-Dist: matplotlib
35
35
  Requires-Dist: numpy
36
- Requires-Dist: pandas >=2.0.0
36
+ Requires-Dist: pandas>=2.0.0
37
37
  Requires-Dist: pyYAML
38
38
  Requires-Dist: scipy
39
- Requires-Dist: shepherd-core[inventory] >=2024.9.1
39
+ Requires-Dist: shepherd-core[inventory]>=2024.11.2
40
40
  Requires-Dist: tqdm
41
- Provides-Extra: dev
42
- Requires-Dist: shepherd-core[dev] ; extra == 'dev'
43
- Requires-Dist: pandas-stubs ; extra == 'dev'
44
41
  Provides-Extra: elf
45
- Requires-Dist: shepherd-core[elf] ; extra == 'elf'
42
+ Requires-Dist: shepherd-core[elf]; extra == "elf"
43
+ Provides-Extra: dev
44
+ Requires-Dist: shepherd-core[dev]; extra == "dev"
45
+ Requires-Dist: pandas-stubs; extra == "dev"
46
46
  Provides-Extra: test
47
- Requires-Dist: shepherd-core[test] ; extra == 'test'
48
- Requires-Dist: pytest ; extra == 'test'
49
- Requires-Dist: pytest-click ; extra == 'test'
50
- Requires-Dist: coverage ; extra == 'test'
47
+ Requires-Dist: shepherd-core[test]; extra == "test"
48
+ Requires-Dist: pytest; extra == "test"
49
+ Requires-Dist: pytest-click; extra == "test"
50
+ Requires-Dist: coverage; extra == "test"
51
51
 
52
52
  # Shepherd-Data-Tool
53
53
 
@@ -0,0 +1,11 @@
1
+ shepherd_data/__init__.py,sha256=yt8ISa_q1NaQ_oqnE_C-JRmbZbpXoK11dAFOOrGEGtk,353
2
+ shepherd_data/cli.py,sha256=6ndpKTZxXYOIogOTxD0Eqha6TtRSLn7_Imd_T6wzidA,13670
3
+ shepherd_data/ivonne.py,sha256=IaHy7RizdaaLAo-cQS-S5xQW_TzHW3dcJjb9hkTLjjY,11900
4
+ shepherd_data/mppt.py,sha256=588KSrLuJfNRKKnnL6ewePLi3zrwaO_PAZypikACrks,3925
5
+ shepherd_data/reader.py,sha256=Fu5QziYNS8ffsWIc4S__Ln_0aF83v-endzpQ5s4seNE,24460
6
+ shepherd_data-2024.11.2.dist-info/METADATA,sha256=WaNBDYAoNy8r2yMAvTZLX5Sojo-j9GhLQdt50_rQ2gg,3431
7
+ shepherd_data-2024.11.2.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
8
+ shepherd_data-2024.11.2.dist-info/entry_points.txt,sha256=6PBfY36A1xNOdzLiz-Qoukya_UzFZAwOapwmRNnPeZ8,56
9
+ shepherd_data-2024.11.2.dist-info/top_level.txt,sha256=7-SCTY-TG1mLY72OVKCaqte1hy-X8woxknIUAD3OIxs,14
10
+ shepherd_data-2024.11.2.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
11
+ shepherd_data-2024.11.2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (74.1.2)
2
+ Generator: setuptools (75.6.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,11 +0,0 @@
1
- shepherd_data/__init__.py,sha256=7qoBbrIem27hArbLNss7PYbdNcvRCzuLegXcJx4skHA,352
2
- shepherd_data/cli.py,sha256=pv17ILFQCdPSyWgmw0rQIhdsYucqeiWFwJn8hft4ga0,15760
3
- shepherd_data/ivonne.py,sha256=IaHy7RizdaaLAo-cQS-S5xQW_TzHW3dcJjb9hkTLjjY,11900
4
- shepherd_data/mppt.py,sha256=588KSrLuJfNRKKnnL6ewePLi3zrwaO_PAZypikACrks,3925
5
- shepherd_data/reader.py,sha256=eShdgACQLwoOtOYIyB3CjvUgJlUEbRDcUso_3m02C9U,20098
6
- shepherd_data-2024.9.1.dist-info/METADATA,sha256=34fTRjrhjV1N_nyJtNCprz91K-IjafBywmdR_TBZZis,3438
7
- shepherd_data-2024.9.1.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
8
- shepherd_data-2024.9.1.dist-info/entry_points.txt,sha256=6PBfY36A1xNOdzLiz-Qoukya_UzFZAwOapwmRNnPeZ8,56
9
- shepherd_data-2024.9.1.dist-info/top_level.txt,sha256=7-SCTY-TG1mLY72OVKCaqte1hy-X8woxknIUAD3OIxs,14
10
- shepherd_data-2024.9.1.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
11
- shepherd_data-2024.9.1.dist-info/RECORD,,