harp-python 0.2.0__tar.gz → 0.4.0__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 (35) hide show
  1. harp_python-0.4.0/.github/workflows/build.yml +37 -0
  2. harp_python-0.4.0/.gitignore +23 -0
  3. {harp-python-0.2.0 → harp_python-0.4.0}/PKG-INFO +21 -4
  4. {harp-python-0.2.0 → harp_python-0.4.0}/README.md +13 -1
  5. harp_python-0.4.0/harp/__init__.py +5 -0
  6. harp_python-0.4.0/harp/io.py +274 -0
  7. {harp-python-0.2.0 → harp_python-0.4.0}/harp/model.py +7 -25
  8. {harp-python-0.2.0 → harp_python-0.4.0}/harp/reader.py +52 -32
  9. {harp-python-0.2.0 → harp_python-0.4.0}/harp/schema.py +12 -11
  10. harp_python-0.4.0/harp/typing.py +13 -0
  11. {harp-python-0.2.0 → harp_python-0.4.0}/harp_python.egg-info/PKG-INFO +21 -4
  12. {harp-python-0.2.0 → harp_python-0.4.0}/harp_python.egg-info/SOURCES.txt +4 -0
  13. {harp-python-0.2.0 → harp_python-0.4.0}/harp_python.egg-info/requires.txt +5 -1
  14. {harp-python-0.2.0 → harp_python-0.4.0}/pyproject.toml +27 -14
  15. {harp-python-0.2.0 → harp_python-0.4.0}/tests/data/device.yml +6 -0
  16. harp_python-0.4.0/tests/data/device_44.bin +0 -0
  17. harp_python-0.4.0/tests/data/empty_0.bin +0 -0
  18. {harp-python-0.2.0 → harp_python-0.4.0}/tests/params.py +7 -2
  19. harp_python-0.4.0/tests/test_io.py +88 -0
  20. {harp-python-0.2.0 → harp_python-0.4.0}/tests/test_reader.py +11 -5
  21. {harp-python-0.2.0 → harp_python-0.4.0}/tests/test_schema.py +1 -0
  22. harp-python-0.2.0/.gitignore +0 -12
  23. harp-python-0.2.0/harp/__init__.py +0 -3
  24. harp-python-0.2.0/harp/io.py +0 -129
  25. harp-python-0.2.0/tests/test_io.py +0 -55
  26. {harp-python-0.2.0 → harp_python-0.4.0}/.gitmodules +0 -0
  27. {harp-python-0.2.0 → harp_python-0.4.0}/LICENSE +0 -0
  28. {harp-python-0.2.0 → harp_python-0.4.0}/MANIFEST.in +0 -0
  29. {harp-python-0.2.0 → harp_python-0.4.0}/harp/common.yml +0 -0
  30. {harp-python-0.2.0 → harp_python-0.4.0}/harp_python.egg-info/dependency_links.txt +0 -0
  31. {harp-python-0.2.0 → harp_python-0.4.0}/harp_python.egg-info/top_level.txt +0 -0
  32. {harp-python-0.2.0 → harp_python-0.4.0}/setup.cfg +0 -0
  33. {harp-python-0.2.0 → harp_python-0.4.0}/tests/__init__.py +0 -0
  34. {harp-python-0.2.0 → harp_python-0.4.0}/tests/data/device_0.bin +0 -0
  35. {harp-python-0.2.0 → harp_python-0.4.0}/tests/data/write_0.bin +0 -0
@@ -0,0 +1,37 @@
1
+ # Builds the python environment; linter and formatting via ruff; type annotations via pyright;
2
+ # tests via pytest; reports test coverage via pytest-cov.
3
+
4
+ name: build
5
+ on:
6
+ push:
7
+ branches: ['*']
8
+ pull_request:
9
+ workflow_dispatch:
10
+
11
+ jobs:
12
+ build_run_tests:
13
+ name: Python ${{ matrix.python-version }} on ${{ matrix.os }}
14
+ runs-on: ${{ matrix.os }}
15
+ if: github.event.pull_request.draft == false
16
+ strategy:
17
+ matrix:
18
+ os: [ubuntu-latest, windows-latest, macos-latest]
19
+ python-version: [3.9, 3.12]
20
+ fail-fast: false
21
+ steps:
22
+ - name: Checkout
23
+ uses: actions/checkout@v4
24
+ - name: Setup Python
25
+ uses: actions/setup-python@v5
26
+ with:
27
+ python-version: ${{ matrix.python-version }}
28
+ cache: 'pip'
29
+ - name: Install dependencies
30
+ run: pip install -e .[dev]
31
+
32
+ - name: ruff
33
+ run: ruff check .
34
+ - name: pyright
35
+ run: pyright .
36
+ - name: pytest
37
+ run: pytest --cov harp
@@ -0,0 +1,23 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+
4
+ # Distribution / packaging
5
+ dist/
6
+ _version.py
7
+ *.egg-info/
8
+ *.egg
9
+
10
+ # IDE
11
+ .vscode/*
12
+
13
+ # misc
14
+ log*.txt
15
+ scratch/
16
+ scratch*.py
17
+
18
+ # Test
19
+ .coverage
20
+
21
+ # Environment
22
+ .venv/
23
+ uv.lock
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: harp-python
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: A low-level interface for loading binary Harp protocol data
5
5
  Author-email: Goncalo Lopes <g.lopes@neurogears.org>
6
6
  License: MIT License
@@ -20,15 +20,32 @@ Requires-Dist: pydantic-yaml
20
20
  Requires-Dist: pandas
21
21
  Provides-Extra: dev
22
22
  Requires-Dist: datamodel-code-generator; extra == "dev"
23
+ Requires-Dist: pandas-stubs; extra == "dev"
23
24
  Requires-Dist: pytest; extra == "dev"
24
- Requires-Dist: black; extra == "dev"
25
+ Requires-Dist: pytest-cov; extra == "dev"
26
+ Requires-Dist: pyright; extra == "dev"
27
+ Requires-Dist: ruff; extra == "dev"
28
+ Requires-Dist: codespell; extra == "dev"
25
29
  Provides-Extra: jupyter
26
30
  Requires-Dist: ipykernel; extra == "jupyter"
27
31
  Requires-Dist: matplotlib; extra == "jupyter"
32
+ Dynamic: license-file
28
33
 
29
34
  # harp-python
30
35
 
31
- A low-level interface to data collected with the [Harp](https://harp-tech.org/) binary protocol.
36
+ A low-level interface to data collected with the [Harp binary protocol](https://harp-tech.org/protocol/BinaryProtocol-8bit.html).
37
+
38
+ ## How to install
39
+
40
+ The source code is currently hosted on [GitHub](https://github.com/harp-tech/harp-python).
41
+
42
+ Binary installers for the latest released version are available at the [Python Package Index (PyPI)](https://pypi.org/project/harp-python) and can be installed using `pip`.
43
+
44
+ ```sh
45
+ pip install harp-python
46
+ ```
47
+
48
+ The list of changes between each release can be found [here](https://github.com/harp-tech/harp-python/releases).
32
49
 
33
50
  ## Data model
34
51
 
@@ -1,6 +1,18 @@
1
1
  # harp-python
2
2
 
3
- A low-level interface to data collected with the [Harp](https://harp-tech.org/) binary protocol.
3
+ A low-level interface to data collected with the [Harp binary protocol](https://harp-tech.org/protocol/BinaryProtocol-8bit.html).
4
+
5
+ ## How to install
6
+
7
+ The source code is currently hosted on [GitHub](https://github.com/harp-tech/harp-python).
8
+
9
+ Binary installers for the latest released version are available at the [Python Package Index (PyPI)](https://pypi.org/project/harp-python) and can be installed using `pip`.
10
+
11
+ ```sh
12
+ pip install harp-python
13
+ ```
14
+
15
+ The list of changes between each release can be found [here](https://github.com/harp-tech/harp-python/releases).
4
16
 
5
17
  ## Data model
6
18
 
@@ -0,0 +1,5 @@
1
+ from harp.io import REFERENCE_EPOCH, MessageType, read, to_buffer, to_file
2
+ from harp.reader import create_reader
3
+ from harp.schema import read_schema
4
+
5
+ __all__ = ["REFERENCE_EPOCH", "MessageType", "read", "to_buffer", "to_file", "create_reader", "read_schema"]
@@ -0,0 +1,274 @@
1
+ from datetime import datetime
2
+ from enum import IntEnum
3
+ from os import PathLike
4
+ from typing import Any, BinaryIO, Optional, Union
5
+
6
+ import numpy as np
7
+ import numpy.typing as npt
8
+ import pandas as pd
9
+ from pandas._typing import Axes
10
+
11
+ from harp.typing import _BufferLike, _FileLike
12
+
13
+ REFERENCE_EPOCH = datetime(1904, 1, 1)
14
+ """The reference epoch for UTC harp time."""
15
+
16
+
17
+ class MessageType(IntEnum):
18
+ """Specifies the type of a Harp message."""
19
+
20
+ NA = 0
21
+ READ = 1
22
+ WRITE = 2
23
+ EVENT = 3
24
+
25
+
26
+ _SECONDS_PER_TICK = 32e-6
27
+ _PAYLOAD_TIMESTAMP_MASK = 0x10
28
+ _messagetypes = [type.name for type in MessageType]
29
+ _dtypefrompayloadtype = {
30
+ 1: np.dtype(np.uint8),
31
+ 2: np.dtype(np.uint16),
32
+ 4: np.dtype(np.uint32),
33
+ 8: np.dtype(np.uint64),
34
+ 129: np.dtype(np.int8),
35
+ 130: np.dtype(np.int16),
36
+ 132: np.dtype(np.int32),
37
+ 136: np.dtype(np.int64),
38
+ 68: np.dtype(np.float32),
39
+ }
40
+ _payloadtypefromdtype = {v: k for k, v in _dtypefrompayloadtype.items()}
41
+
42
+
43
+ def read(
44
+ file_or_buf: Union[_FileLike, _BufferLike],
45
+ address: Optional[int] = None,
46
+ dtype: Optional[np.dtype] = None,
47
+ length: Optional[int] = None,
48
+ columns: Optional[Axes] = None,
49
+ epoch: Optional[datetime] = None,
50
+ keep_type: bool = False,
51
+ ):
52
+ """Read single-register Harp data from the specified file or buffer.
53
+
54
+ Parameters
55
+ ----------
56
+ file_or_buf
57
+ File path, open file object, or buffer containing binary data from
58
+ a single device register.
59
+ address
60
+ Expected register address. If specified, the address of
61
+ the first message is used for validation.
62
+ dtype
63
+ Expected data type of the register payload. If specified, the
64
+ payload type of the first message is used for validation.
65
+ length
66
+ Expected number of elements in register payload. If specified, the
67
+ payload length of the first message is used for validation.
68
+ columns
69
+ The optional column labels to use for the data values.
70
+ epoch
71
+ Reference datetime at which time zero begins. If specified,
72
+ the result data frame will have a datetime index.
73
+ keep_type
74
+ Specifies whether to include a column with the message type.
75
+
76
+ Returns
77
+ -------
78
+ A pandas data frame containing message data, sorted by time.
79
+ """
80
+ if isinstance(file_or_buf, (str, PathLike, BinaryIO)) or hasattr(file_or_buf, "readinto"):
81
+ # TODO: in the below we ignore the type as otherwise
82
+ # we have no way to runtime check _IOProtocol
83
+ data = np.fromfile(file_or_buf, dtype=np.uint8) # type: ignore
84
+ else:
85
+ data = np.frombuffer(file_or_buf, dtype=np.uint8)
86
+
87
+ if len(data) == 0:
88
+ return pd.DataFrame(
89
+ columns=columns,
90
+ index=pd.DatetimeIndex([], name="Time")
91
+ if epoch
92
+ else pd.Index([], dtype=np.float64, name="Time"),
93
+ )
94
+
95
+ if address is not None and address != data[2]:
96
+ raise ValueError(f"expected address {address} but got {data[2]}")
97
+
98
+ index = None
99
+ stride = int(data[1] + 2)
100
+ nrows = len(data) // stride
101
+ payloadtype = data[4]
102
+ payloadoffset = 5
103
+ if payloadtype & _PAYLOAD_TIMESTAMP_MASK != 0:
104
+ seconds = np.ndarray(nrows, dtype=np.uint32, buffer=data, offset=payloadoffset, strides=stride)
105
+ payloadoffset += 4
106
+ micros = np.ndarray(nrows, dtype=np.uint16, buffer=data, offset=payloadoffset, strides=stride)
107
+ payloadoffset += 2
108
+ time = micros * _SECONDS_PER_TICK + seconds
109
+ payloadtype = payloadtype & ~np.uint8(_PAYLOAD_TIMESTAMP_MASK)
110
+ if epoch is not None:
111
+ time = epoch + pd.to_timedelta(time, "s") # type: ignore
112
+ index = pd.Series(time)
113
+ index.name = "Time"
114
+
115
+ payloadsize = stride - payloadoffset - 1
116
+ payloadtype = _dtypefrompayloadtype[payloadtype]
117
+ if dtype is not None and dtype != payloadtype:
118
+ raise ValueError(f"expected payload type {dtype} but got {payloadtype}")
119
+
120
+ elementsize = payloadtype.itemsize
121
+ payloadshape = (nrows, payloadsize // elementsize)
122
+ if length is not None and length != payloadshape[1]:
123
+ raise ValueError(f"expected payload length {length} but got {payloadshape[1]}")
124
+
125
+ payload = np.ndarray(
126
+ payloadshape,
127
+ dtype=payloadtype,
128
+ buffer=data,
129
+ offset=payloadoffset,
130
+ strides=(stride, elementsize),
131
+ )
132
+
133
+ result = pd.DataFrame(payload, index=index, columns=columns)
134
+ if keep_type:
135
+ msgtype = np.ndarray(nrows, dtype=np.uint8, buffer=data, offset=0, strides=stride)
136
+ msgtype = pd.Categorical.from_codes(msgtype, categories=_messagetypes) # type: ignore
137
+ result[MessageType.__name__] = msgtype
138
+ return result
139
+
140
+
141
+ def to_file(
142
+ data: pd.DataFrame,
143
+ file: _FileLike,
144
+ address: int,
145
+ dtype: Optional[np.dtype] = None,
146
+ length: Optional[int] = None,
147
+ port: Optional[int] = None,
148
+ epoch: Optional[datetime] = None,
149
+ message_type: Optional[MessageType] = None,
150
+ ):
151
+ """Write single-register Harp data to the specified file.
152
+
153
+ Parameters
154
+ ----------
155
+ data
156
+ Pandas data frame containing message payload.
157
+ file
158
+ File path, or open file object in which to store binary data from
159
+ a single device register.
160
+ address
161
+ Register address used to identify all formatted Harp messages.
162
+ dtype
163
+ Data type of the register payload. If specified, all data will
164
+ be converted before formatting the binary payload.
165
+ length
166
+ Expected number of elements in register payload. If specified, the
167
+ number of columns in the input data frame is validated.
168
+ port
169
+ Optional port value used for all formatted Harp messages.
170
+ epoch
171
+ Reference datetime at which time zero begins. If specified,
172
+ the input data frame must have a datetime index.
173
+ message_type
174
+ Optional message type used for all formatted Harp messages.
175
+ If not specified, data must contain a MessageType column.
176
+ """
177
+ buffer = to_buffer(data, address, dtype, port, length, epoch, message_type)
178
+ buffer.tofile(file)
179
+
180
+
181
+ def to_buffer(
182
+ data: pd.DataFrame,
183
+ address: int,
184
+ dtype: Optional[np.dtype] = None,
185
+ length: Optional[int] = None,
186
+ port: Optional[int] = None,
187
+ epoch: Optional[datetime] = None,
188
+ message_type: Optional[MessageType] = None,
189
+ ) -> npt.NDArray[np.uint8]:
190
+ """Convert single-register Harp data to a flat binary buffer.
191
+
192
+ Parameters
193
+ ----------
194
+ data
195
+ Pandas data frame containing message payload.
196
+ address
197
+ Register address used to identify all formatted Harp messages.
198
+ dtype
199
+ Data type of the register payload. If specified, all data will
200
+ be converted before formatting the binary payload.
201
+ length
202
+ Expected number of elements in register payload. If specified, the
203
+ number of columns in the input data frame is validated.
204
+ port
205
+ Optional port value used for all formatted Harp messages.
206
+ epoch
207
+ Reference datetime at which time zero begins. If specified,
208
+ the input data frame must have a datetime index.
209
+ message_type
210
+ Optional message type used for all formatted Harp messages.
211
+ If not specified, data must contain a MessageType column.
212
+
213
+ Returns
214
+ -------
215
+ An array object containing message data formatted according
216
+ to the Harp binary protocol.
217
+ """
218
+ nrows = len(data)
219
+ if nrows == 0:
220
+ return np.empty(0, dtype=np.uint8)
221
+
222
+ if MessageType.__name__ in data.columns:
223
+ msgtype = data[MessageType.__name__].cat.codes
224
+ payload = data[data.columns.drop(MessageType.__name__)].values
225
+ elif message_type is not None:
226
+ msgtype = message_type
227
+ payload = data.values
228
+ else:
229
+ raise ValueError(f"message type must be specified either in the data or as argument")
230
+
231
+ time = data.index
232
+ is_timestamped = True
233
+ if epoch is not None:
234
+ if not isinstance(time, pd.DatetimeIndex):
235
+ raise ValueError(f"expected datetime index to encode with epoch but got {time.inferred_type}")
236
+ time = (time - epoch).total_seconds()
237
+ elif isinstance(time, pd.RangeIndex):
238
+ is_timestamped = False
239
+
240
+ if dtype is not None:
241
+ payload = payload.astype(dtype, copy=False)
242
+
243
+ ncols = payload.shape[1]
244
+ if length is not None and ncols != length:
245
+ raise ValueError(f"expected payload length {length} but got {ncols}")
246
+
247
+ if port is None:
248
+ port = 255
249
+
250
+ payloadtype = _payloadtypefromdtype[payload.dtype]
251
+ payloadlength = ncols * payload.dtype.itemsize
252
+ stride = payloadlength + 6
253
+ if is_timestamped:
254
+ payloadtype |= _PAYLOAD_TIMESTAMP_MASK
255
+ stride += 6
256
+
257
+ buffer = np.empty((nrows, stride), dtype=np.uint8)
258
+ buffer[:, 0] = msgtype
259
+ buffer[:, 1:5] = [stride - 2, address, port, payloadtype]
260
+
261
+ payloadoffset = 5
262
+ if is_timestamped:
263
+ seconds = time.astype(np.uint32)
264
+ micros = np.around(((time - seconds) / _SECONDS_PER_TICK).values).astype(np.uint16)
265
+ buffer[:, 5:9] = np.ndarray((nrows, 4), dtype=np.uint8, buffer=seconds.values)
266
+ buffer[:, 9:11] = np.ndarray((nrows, 2), dtype=np.uint8, buffer=micros)
267
+ payloadoffset += 6
268
+
269
+ payloadstop = payloadoffset + payloadlength
270
+ buffer[:, payloadoffset:payloadstop] = np.ndarray(
271
+ (nrows, payloadlength), dtype=np.uint8, buffer=np.ascontiguousarray(payload)
272
+ )
273
+ buffer[:, -1] = np.sum(buffer[:, 0:-1], axis=1, dtype=np.uint8)
274
+ return buffer.reshape(-1)
@@ -5,17 +5,9 @@
5
5
  from __future__ import annotations
6
6
 
7
7
  from enum import Enum
8
- from typing import Dict, List, Optional, Union
9
- from typing_extensions import Annotated
8
+ from typing import Annotated, Dict, List, Optional, Union
10
9
 
11
- from pydantic import (
12
- BaseModel,
13
- BeforeValidator,
14
- ConfigDict,
15
- Field,
16
- RootModel,
17
- field_serializer,
18
- )
10
+ from pydantic import BaseModel, BeforeValidator, ConfigDict, Field, RootModel, field_serializer
19
11
 
20
12
 
21
13
  class PayloadType(str, Enum):
@@ -136,16 +128,12 @@ class Visibility(Enum):
136
128
  class Register(BaseModel):
137
129
  address: Annotated[
138
130
  int,
139
- Field(
140
- le=255, description="Specifies the unique 8-bit address of the register."
141
- ),
131
+ Field(le=255, description="Specifies the unique 8-bit address of the register."),
142
132
  ]
143
133
  type: Annotated[PayloadType, BeforeValidator(lambda v: PayloadType[v])]
144
134
  length: Annotated[
145
135
  Optional[int],
146
- Field(
147
- ge=1, default=1, description="Specifies the length of the register payload."
148
- ),
136
+ Field(ge=1, default=1, description="Specifies the length of the register payload."),
149
137
  ]
150
138
  access: Union[Access, List[Access]] = Field(
151
139
  ..., description="Specifies the expected use of the register."
@@ -191,12 +179,6 @@ class Registers(BaseModel):
191
179
 
192
180
  class Model(Registers):
193
181
  device: str = Field(..., description="Specifies the name of the device.")
194
- whoAmI: int = Field(
195
- ..., description="Specifies the unique identifier for this device type."
196
- )
197
- firmwareVersion: str = Field(
198
- ..., description="Specifies the semantic version of the device firmware."
199
- )
200
- hardwareTargets: str = Field(
201
- ..., description="Specifies the semantic version of the device hardware."
202
- )
182
+ whoAmI: int = Field(..., description="Specifies the unique identifier for this device type.")
183
+ firmwareVersion: str = Field(..., description="Specifies the semantic version of the device firmware.")
184
+ hardwareTargets: str = Field(..., description="Specifies the semantic version of the device hardware.")
@@ -1,22 +1,26 @@
1
1
  import os
2
+ from collections import UserDict
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+ from functools import partial
2
6
  from math import log2
3
7
  from os import PathLike
4
8
  from pathlib import Path
5
- from datetime import datetime
6
- from functools import partial
7
- from dataclasses import dataclass
9
+ from typing import Callable, Iterable, Mapping, Optional, Protocol, Union
10
+
8
11
  from numpy import dtype
9
12
  from pandas import DataFrame, Series
10
- from typing import Any, BinaryIO, Callable, Iterable, Optional, Protocol, Union
11
13
  from pandas._typing import Axes
12
- from harp.model import BitMask, GroupMask, Model, PayloadMember, Register
14
+
13
15
  from harp.io import MessageType, read
16
+ from harp.model import BitMask, GroupMask, Model, PayloadMember, Register
14
17
  from harp.schema import read_schema
18
+ from harp.typing import _BufferLike, _FileLike
15
19
 
16
20
 
17
21
  @dataclass
18
22
  class _ReaderParams:
19
- path: Path
23
+ base_path: Path
20
24
  epoch: Optional[datetime] = None
21
25
  keep_type: bool = False
22
26
 
@@ -24,11 +28,10 @@ class _ReaderParams:
24
28
  class _ReadRegister(Protocol):
25
29
  def __call__(
26
30
  self,
27
- file: Optional[Union[str, bytes, PathLike[Any], BinaryIO]] = None,
31
+ file_or_buf: Optional[Union[_FileLike, _BufferLike]] = None,
28
32
  epoch: Optional[datetime] = None,
29
33
  keep_type: bool = False,
30
- ) -> DataFrame:
31
- ...
34
+ ) -> DataFrame: ...
32
35
 
33
36
 
34
37
  class RegisterReader:
@@ -44,13 +47,27 @@ class RegisterReader:
44
47
  self.read = read
45
48
 
46
49
 
50
+ class RegisterMap(UserDict[str, RegisterReader]):
51
+ _address_map: Mapping[int, RegisterReader]
52
+
53
+ def __init__(self, registers: Mapping[str, RegisterReader]) -> None:
54
+ super().__init__(registers)
55
+ self._address_map = {value.register.address: value for value in registers.values()}
56
+
57
+ def __getitem__(self, key: Union[str, int]) -> RegisterReader:
58
+ if isinstance(key, int):
59
+ return self._address_map[key]
60
+ else:
61
+ return super().__getitem__(key)
62
+
63
+
47
64
  class DeviceReader:
48
65
  device: Model
49
- registers: dict[str, RegisterReader]
66
+ registers: RegisterMap
50
67
 
51
- def __init__(self, device: Model, registers: dict[str, RegisterReader]) -> None:
68
+ def __init__(self, device: Model, registers: Mapping[str, RegisterReader]) -> None:
52
69
  self.device = device
53
- self.registers = registers
70
+ self.registers = RegisterMap(registers)
54
71
 
55
72
  def __dir__(self) -> Iterable[str]:
56
73
  return self.registers.keys()
@@ -65,12 +82,12 @@ def _compose_parser(
65
82
  params: _ReaderParams,
66
83
  ) -> Callable[..., DataFrame]:
67
84
  def parser(
68
- file: Optional[Union[str, bytes, PathLike[Any], BinaryIO]] = None,
85
+ data,
69
86
  columns: Optional[Axes] = None,
70
87
  epoch: Optional[datetime] = params.epoch,
71
88
  keep_type: bool = params.keep_type,
72
89
  ):
73
- df = g(file, columns, epoch, keep_type)
90
+ df = g(data, columns, epoch, keep_type)
74
91
  result = f(df)
75
92
  type_col = df.get(MessageType.__name__)
76
93
  if type_col is not None:
@@ -151,16 +168,16 @@ def _create_payloadmember_parser(device: Model, member: PayloadMember):
151
168
 
152
169
  def _create_register_reader(register: Register, params: _ReaderParams):
153
170
  def reader(
154
- file: Optional[Union[str, bytes, PathLike[Any], BinaryIO]] = None,
171
+ file_or_buf: Optional[Union[_FileLike, _BufferLike]] = None,
155
172
  columns: Optional[Axes] = None,
156
173
  epoch: Optional[datetime] = params.epoch,
157
174
  keep_type: bool = params.keep_type,
158
175
  ):
159
- if file is None:
160
- file = f"{params.path}_{register.address}.bin"
176
+ if file_or_buf is None:
177
+ file_or_buf = f"{params.base_path}_{register.address}.bin"
161
178
 
162
179
  data = read(
163
- file,
180
+ file_or_buf,
164
181
  address=register.address,
165
182
  dtype=dtype(register.type),
166
183
  length=register.length,
@@ -173,7 +190,7 @@ def _create_register_reader(register: Register, params: _ReaderParams):
173
190
  return reader
174
191
 
175
192
 
176
- def _create_register_parser(device: Model, name: str, params: _ReaderParams):
193
+ def _create_register_handler(device: Model, name: str, params: _ReaderParams):
177
194
  register = device.registers[name]
178
195
  reader = _create_register_reader(register, params)
179
196
 
@@ -181,29 +198,34 @@ def _create_register_parser(device: Model, name: str, params: _ReaderParams):
181
198
  key = register.maskType.root
182
199
  bitMask = None if device.bitMasks is None else device.bitMasks.get(key)
183
200
  if bitMask is not None:
184
- parser = _create_bitmask_parser(bitMask)
185
- reader = _compose_parser(parser, reader, params)
201
+ bitmask_parser = _create_bitmask_parser(bitMask)
202
+ reader = _compose_parser(bitmask_parser, reader, params)
186
203
  return RegisterReader(register, reader)
187
204
 
188
205
  groupMask = None if device.groupMasks is None else device.groupMasks.get(key)
189
206
  if groupMask is not None:
190
- parser = _create_groupmask_parser(name, groupMask)
191
- reader = _compose_parser(parser, reader, params)
207
+ groupmask_parser = _create_groupmask_parser(name, groupMask)
208
+ reader = _compose_parser(groupmask_parser, reader, params)
192
209
  return RegisterReader(register, reader)
193
210
 
194
211
  if register.payloadSpec is not None:
195
- payload_parsers = [
212
+ member_parsers = [
196
213
  (key, _create_payloadmember_parser(device, member))
197
214
  for key, member in register.payloadSpec.items()
198
215
  ]
199
216
 
200
- def parser(df: DataFrame):
201
- return DataFrame({n: f(df) for n, f in payload_parsers}, index=df.index)
217
+ def payload_parser(df: DataFrame):
218
+ return DataFrame({n: f(df) for n, f in member_parsers}, index=df.index)
202
219
 
203
- reader = _compose_parser(parser, reader, params)
220
+ reader = _compose_parser(payload_parser, reader, params)
204
221
  return RegisterReader(register, reader)
205
222
 
206
- reader = partial(reader, columns=[name])
223
+ columns = (
224
+ [name]
225
+ if register.length is None or register.length == 1
226
+ else [f"{name}_{i}" for i in range(register.length)]
227
+ )
228
+ reader = partial(reader, columns=columns)
207
229
  return RegisterReader(register, reader)
208
230
 
209
231
 
@@ -214,7 +236,7 @@ def create_reader(
214
236
  keep_type: bool = False,
215
237
  ):
216
238
  """Creates a device reader object from the specified dataset or schema.
217
-
239
+
218
240
  Parameters
219
241
  ----------
220
242
  device
@@ -249,9 +271,7 @@ def create_reader(
249
271
  base_path = path / device.device if is_dir else path.parent / device.device
250
272
 
251
273
  reg_readers = {
252
- name: _create_register_parser(
253
- device, name, _ReaderParams(base_path, epoch, keep_type)
254
- )
274
+ name: _create_register_handler(device, name, _ReaderParams(base_path, epoch, keep_type))
255
275
  for name in device.registers.keys()
256
276
  }
257
277
  return DeviceReader(device, reg_readers)
@@ -1,21 +1,24 @@
1
+ from importlib import resources
1
2
  from os import PathLike
2
3
  from typing import TextIO, Union
3
- from harp.model import Model, Registers
4
+
4
5
  from pydantic_yaml import parse_yaml_raw_as
5
- from importlib import resources
6
+
7
+ from harp.model import Model, Registers
6
8
 
7
9
 
8
10
  def _read_common_registers() -> Registers:
11
+ if __package__ is None:
12
+ raise ValueError("__package__ is None: unable to read common registers")
13
+
9
14
  file = resources.files(__package__) / "common.yml"
10
- with file.open("rt") as fileIO:
15
+ with file.open("r") as fileIO:
11
16
  return parse_yaml_raw_as(Registers, fileIO.read())
12
17
 
13
18
 
14
- def read_schema(
15
- file: Union[str, PathLike, TextIO], include_common_registers: bool = True
16
- ) -> Model:
19
+ def read_schema(file: Union[str, PathLike, TextIO], include_common_registers: bool = True) -> Model:
17
20
  """Read and parse a device schema from the specified file.
18
-
21
+
19
22
  Parameters
20
23
  ----------
21
24
  file
@@ -35,14 +38,12 @@ def read_schema(
35
38
  return read_schema(fileIO)
36
39
  else:
37
40
  schema = parse_yaml_raw_as(Model, file.read())
38
- if not "WhoAmI" in schema.registers and include_common_registers:
41
+ if "WhoAmI" not in schema.registers and include_common_registers:
39
42
  common = _read_common_registers()
40
43
  schema.registers = dict(common.registers, **schema.registers)
41
44
  if common.bitMasks:
42
45
  schema.bitMasks = (
43
- common.bitMasks
44
- if schema.bitMasks is None
45
- else dict(common.bitMasks, **schema.bitMasks)
46
+ common.bitMasks if schema.bitMasks is None else dict(common.bitMasks, **schema.bitMasks)
46
47
  )
47
48
  if common.groupMasks:
48
49
  schema.groupMasks = (
@@ -0,0 +1,13 @@
1
+ import mmap
2
+ import sys
3
+ from os import PathLike
4
+ from typing import Any, BinaryIO, Union
5
+
6
+ from numpy.typing import NDArray
7
+
8
+ if sys.version_info >= (3, 12):
9
+ from collections.abc import Buffer as _BufferLike
10
+ else:
11
+ _BufferLike = Union[bytes, bytearray, memoryview, mmap.mmap, NDArray[Any]]
12
+
13
+ _FileLike = Union[str, PathLike[str], BinaryIO]
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: harp-python
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: A low-level interface for loading binary Harp protocol data
5
5
  Author-email: Goncalo Lopes <g.lopes@neurogears.org>
6
6
  License: MIT License
@@ -20,15 +20,32 @@ Requires-Dist: pydantic-yaml
20
20
  Requires-Dist: pandas
21
21
  Provides-Extra: dev
22
22
  Requires-Dist: datamodel-code-generator; extra == "dev"
23
+ Requires-Dist: pandas-stubs; extra == "dev"
23
24
  Requires-Dist: pytest; extra == "dev"
24
- Requires-Dist: black; extra == "dev"
25
+ Requires-Dist: pytest-cov; extra == "dev"
26
+ Requires-Dist: pyright; extra == "dev"
27
+ Requires-Dist: ruff; extra == "dev"
28
+ Requires-Dist: codespell; extra == "dev"
25
29
  Provides-Extra: jupyter
26
30
  Requires-Dist: ipykernel; extra == "jupyter"
27
31
  Requires-Dist: matplotlib; extra == "jupyter"
32
+ Dynamic: license-file
28
33
 
29
34
  # harp-python
30
35
 
31
- A low-level interface to data collected with the [Harp](https://harp-tech.org/) binary protocol.
36
+ A low-level interface to data collected with the [Harp binary protocol](https://harp-tech.org/protocol/BinaryProtocol-8bit.html).
37
+
38
+ ## How to install
39
+
40
+ The source code is currently hosted on [GitHub](https://github.com/harp-tech/harp-python).
41
+
42
+ Binary installers for the latest released version are available at the [Python Package Index (PyPI)](https://pypi.org/project/harp-python) and can be installed using `pip`.
43
+
44
+ ```sh
45
+ pip install harp-python
46
+ ```
47
+
48
+ The list of changes between each release can be found [here](https://github.com/harp-tech/harp-python/releases).
32
49
 
33
50
  ## Data model
34
51
 
@@ -4,12 +4,14 @@ LICENSE
4
4
  MANIFEST.in
5
5
  README.md
6
6
  pyproject.toml
7
+ .github/workflows/build.yml
7
8
  harp/__init__.py
8
9
  harp/common.yml
9
10
  harp/io.py
10
11
  harp/model.py
11
12
  harp/reader.py
12
13
  harp/schema.py
14
+ harp/typing.py
13
15
  harp_python.egg-info/PKG-INFO
14
16
  harp_python.egg-info/SOURCES.txt
15
17
  harp_python.egg-info/dependency_links.txt
@@ -22,4 +24,6 @@ tests/test_reader.py
22
24
  tests/test_schema.py
23
25
  tests/data/device.yml
24
26
  tests/data/device_0.bin
27
+ tests/data/device_44.bin
28
+ tests/data/empty_0.bin
25
29
  tests/data/write_0.bin
@@ -3,8 +3,12 @@ pandas
3
3
 
4
4
  [dev]
5
5
  datamodel-code-generator
6
+ pandas-stubs
6
7
  pytest
7
- black
8
+ pytest-cov
9
+ pyright
10
+ ruff
11
+ codespell
8
12
 
9
13
  [jupyter]
10
14
  ipykernel
@@ -31,9 +31,14 @@ classifiers = [
31
31
  [project.optional-dependencies]
32
32
  dev = [
33
33
  "datamodel-code-generator",
34
+ "pandas-stubs",
34
35
  "pytest",
35
- "black"
36
+ "pytest-cov",
37
+ "pyright",
38
+ "ruff",
39
+ "codespell"
36
40
  ]
41
+
37
42
  jupyter = [
38
43
  "ipykernel",
39
44
  "matplotlib"
@@ -55,16 +60,24 @@ include = ["harp*"]
55
60
 
56
61
  [tool.setuptools_scm]
57
62
 
58
- [tool.black]
59
- line-length = 88
60
- target-version = ['py39']
61
- include = '\.pyi?$'
62
- extend-exclude = '''
63
- # A regex preceded with ^/ will apply only to files and directories
64
- # in the root of the project.
65
- (
66
- ^/LICENSE
67
- ^/README.md
68
- | reflex-generator
69
- )
70
- '''
63
+ [tool.ruff]
64
+ line-length = 108
65
+ target-version = "py39"
66
+ exclude = [
67
+ "reflex-generator"
68
+ ]
69
+
70
+ [tool.ruff.lint]
71
+ select = ["I"]
72
+
73
+ [tool.pyright]
74
+ venvPath = "."
75
+ venv = ".venv"
76
+ exclude = [
77
+ ".venv/*",
78
+ "reflex-generator"
79
+ ]
80
+
81
+ [tool.codespell]
82
+ skip = '.git,*.pdf,*.svg'
83
+ ignore-words-list = 'nd'
@@ -18,6 +18,12 @@ registers:
18
18
  type: U8
19
19
  maskType: DigitalInputs
20
20
  description: Reports the state of the digital input lines.
21
+ AnalogData:
22
+ address: 44
23
+ type: S16
24
+ length: 3
25
+ access: Event
26
+ description: Reports the current values of the analog input lines.
21
27
  bitMasks:
22
28
  DigitalInputs:
23
29
  description: Specifies the state of the digital input lines.
File without changes
@@ -1,8 +1,11 @@
1
- import numpy as np
1
+ from dataclasses import dataclass
2
+ from datetime import datetime
2
3
  from os import PathLike
3
4
  from pathlib import Path
4
- from dataclasses import dataclass
5
5
  from typing import Iterable, Optional, Type, Union
6
+
7
+ import numpy as np
8
+
6
9
  from harp.model import Model
7
10
 
8
11
  datapath = Path(__file__).parent
@@ -17,6 +20,8 @@ class DataFileParam:
17
20
  expected_dtype: Optional[np.dtype] = None
18
21
  expected_length: Optional[int] = None
19
22
  expected_error: Optional[Type[BaseException]] = None
23
+ repeat_data: Optional[int] = None
24
+ epoch: Optional[datetime] = None
20
25
  keep_type: bool = False
21
26
 
22
27
  def __post_init__(self):
@@ -0,0 +1,88 @@
1
+ from contextlib import nullcontext
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+ import pytest
6
+ from pytest import mark
7
+
8
+ from harp.io import REFERENCE_EPOCH, MessageType, read, to_buffer
9
+ from tests.params import DataFileParam
10
+
11
+ testdata = [
12
+ DataFileParam(path="data/device_0.bin", expected_rows=1),
13
+ DataFileParam(
14
+ path="data/device_0.bin",
15
+ expected_rows=1,
16
+ expected_address=1, # actual address is 0
17
+ expected_error=ValueError,
18
+ ),
19
+ DataFileParam(
20
+ path="data/device_0.bin",
21
+ expected_rows=1,
22
+ expected_dtype=np.dtype("uint8"), # actual dtype is uint16
23
+ expected_error=ValueError,
24
+ ),
25
+ DataFileParam(
26
+ path="data/device_0.bin",
27
+ expected_rows=1,
28
+ expected_length=2, # actual length is 1
29
+ expected_error=ValueError,
30
+ ),
31
+ DataFileParam(path="data/write_0.bin", expected_address=0, expected_rows=4),
32
+ DataFileParam(path="data/write_0.bin", expected_address=0, expected_rows=4, keep_type=True),
33
+ DataFileParam(path="data/device_0.bin", expected_rows=300, repeat_data=300),
34
+ DataFileParam(path="data/device_0.bin", expected_rows=1, epoch=REFERENCE_EPOCH),
35
+ DataFileParam(path="data/empty_0.bin", expected_rows=0, epoch=REFERENCE_EPOCH),
36
+ DataFileParam(path="data/empty_0.bin", expected_rows=0),
37
+ ]
38
+
39
+
40
+ @mark.parametrize("dataFile", testdata)
41
+ def test_read(dataFile: DataFileParam):
42
+ context = pytest.raises if dataFile.expected_error else nullcontext
43
+ with context(dataFile.expected_error): # type: ignore
44
+ file_or_buf = dataFile.path
45
+ if dataFile.repeat_data:
46
+ with open(file_or_buf, "rb") as f:
47
+ file_or_buf = f.read() * dataFile.repeat_data
48
+
49
+ data = read(
50
+ file_or_buf,
51
+ address=dataFile.expected_address,
52
+ dtype=dataFile.expected_dtype,
53
+ length=dataFile.expected_length,
54
+ epoch=dataFile.epoch,
55
+ keep_type=dataFile.keep_type,
56
+ )
57
+
58
+ assert len(data) == dataFile.expected_rows
59
+ assert isinstance(data.index, pd.DatetimeIndex if dataFile.epoch else pd.Index)
60
+ if dataFile.keep_type:
61
+ assert MessageType.__name__ in data.columns and data[MessageType.__name__].dtype == "category"
62
+
63
+ if dataFile.expected_cols:
64
+ for col in dataFile.expected_cols:
65
+ assert col in data.columns
66
+
67
+
68
+ writedata = [
69
+ DataFileParam(path="data/device_0.bin", expected_rows=1, expected_address=0, keep_type=True),
70
+ ]
71
+
72
+
73
+ @mark.parametrize("dataFile", writedata)
74
+ def test_write(dataFile: DataFileParam):
75
+ if dataFile.expected_address is None:
76
+ raise AssertionError("expected address must be defined for all write tests")
77
+
78
+ buffer = np.fromfile(dataFile.path, np.uint8)
79
+ data = read(
80
+ buffer,
81
+ address=dataFile.expected_address,
82
+ dtype=dataFile.expected_dtype,
83
+ length=dataFile.expected_length,
84
+ keep_type=dataFile.keep_type,
85
+ )
86
+ assert len(data) == dataFile.expected_rows
87
+ write_buffer = to_buffer(data, address=dataFile.expected_address, length=dataFile.expected_length)
88
+ assert np.array_equal(buffer, write_buffer)
@@ -1,5 +1,6 @@
1
- import numpy as np
1
+ import pandas as pd
2
2
  from pytest import mark
3
+
3
4
  from harp.io import REFERENCE_EPOCH, MessageType
4
5
  from harp.reader import create_reader
5
6
  from tests.params import DeviceSchemaParam
@@ -8,12 +9,12 @@ testdata = [
8
9
  DeviceSchemaParam(
9
10
  path="data",
10
11
  expected_whoAmI=0,
11
- expected_registers=["DigitalInputMode"],
12
+ expected_registers=["AnalogData"],
12
13
  ),
13
14
  DeviceSchemaParam(
14
15
  path="data/device.yml",
15
16
  expected_whoAmI=0,
16
- expected_registers=["DigitalInputMode"],
17
+ expected_registers=["AnalogData"],
17
18
  ),
18
19
  ]
19
20
 
@@ -25,8 +26,13 @@ def test_create_reader(schemaFile: DeviceSchemaParam):
25
26
 
26
27
  whoAmI = reader.WhoAmI.read()
27
28
  assert reader.device.whoAmI == whoAmI.iloc[0, 0]
28
- assert whoAmI.index.dtype.type == np.datetime64
29
+ assert isinstance(whoAmI.index, pd.DatetimeIndex)
29
30
 
30
31
  whoAmI = reader.WhoAmI.read(epoch=None, keep_type=True)
31
- assert whoAmI.index.dtype.type == np.float64
32
+ assert isinstance(whoAmI.index, pd.Index)
32
33
  assert whoAmI.iloc[0, -1] == MessageType.READ.name
34
+
35
+ if schemaFile.expected_registers:
36
+ for register_name in schemaFile.expected_registers:
37
+ data = reader.registers[register_name].read()
38
+ assert isinstance(data.index, pd.DatetimeIndex)
@@ -1,4 +1,5 @@
1
1
  from pytest import mark
2
+
2
3
  from harp.schema import read_schema
3
4
  from tests.params import DeviceSchemaParam
4
5
 
@@ -1,12 +0,0 @@
1
- # Environment files
2
- .vscode
3
- .venv
4
-
5
- # Python temp files
6
- __pycache__
7
- *.pyc
8
- *.egg-info
9
- dist
10
-
11
- # Data files
12
- /data
@@ -1,3 +0,0 @@
1
- from harp.io import REFERENCE_EPOCH, MessageType, read
2
- from harp.reader import create_reader
3
- from harp.schema import read_schema
@@ -1,129 +0,0 @@
1
- from datetime import datetime
2
- from enum import IntEnum
3
- from os import PathLike
4
- from typing import Any, BinaryIO, Optional, Union
5
- from pandas._typing import Axes
6
- import numpy as np
7
- import pandas as pd
8
-
9
- REFERENCE_EPOCH = datetime(1904, 1, 1)
10
- """The reference epoch for UTC harp time."""
11
-
12
-
13
- class MessageType(IntEnum):
14
- """Specifies the type of a Harp message."""
15
-
16
- NA = 0
17
- READ = 1
18
- WRITE = 2
19
- EVENT = 3
20
-
21
-
22
- _SECONDS_PER_TICK = 32e-6
23
- _messagetypes = [type.name for type in MessageType]
24
- _payloadtypes = {
25
- 1: np.dtype(np.uint8),
26
- 2: np.dtype(np.uint16),
27
- 4: np.dtype(np.uint32),
28
- 8: np.dtype(np.uint64),
29
- 129: np.dtype(np.int8),
30
- 130: np.dtype(np.int16),
31
- 132: np.dtype(np.int32),
32
- 136: np.dtype(np.int64),
33
- 68: np.dtype(np.float32),
34
- }
35
-
36
-
37
- def read(
38
- file: Union[str, bytes, PathLike[Any], BinaryIO],
39
- address: Optional[int] = None,
40
- dtype: Optional[np.dtype] = None,
41
- length: Optional[int] = None,
42
- columns: Optional[Axes] = None,
43
- epoch: Optional[datetime] = None,
44
- keep_type: bool = False,
45
- ):
46
- """Read single-register Harp data from the specified file.
47
-
48
- Parameters
49
- ----------
50
- file
51
- Open file object or filename containing binary data from
52
- a single device register.
53
- address
54
- Expected register address. If specified, the address of
55
- the first message in the file is used for validation.
56
- dtype
57
- Expected data type of the register payload. If specified, the
58
- payload type of the first message in the file is used for validation.
59
- length
60
- Expected number of elements in register payload. If specified, the
61
- payload length of the first message in the file is used for validation.
62
- columns
63
- The optional column labels to use for the data values.
64
- epoch
65
- Reference datetime at which time zero begins. If specified,
66
- the result data frame will have a datetime index.
67
- keep_type
68
- Specifies whether to include a column with the message type.
69
-
70
- Returns
71
- -------
72
- A pandas data frame containing message data, sorted by time.
73
- """
74
- data = np.fromfile(file, dtype=np.uint8)
75
- if len(data) == 0:
76
- return pd.DataFrame(
77
- columns=columns, index=pd.Index([], dtype=np.float64, name="Time")
78
- )
79
-
80
- if address is not None and address != data[2]:
81
- raise ValueError(f"expected address {address} but got {data[2]}")
82
-
83
- index = None
84
- stride = data[1] + 2
85
- nrows = len(data) // stride
86
- payloadtype = data[4]
87
- payloadoffset = 5
88
- if payloadtype & 0x10 != 0:
89
- seconds = np.ndarray(
90
- nrows, dtype=np.uint32, buffer=data, offset=payloadoffset, strides=stride
91
- )
92
- payloadoffset += 4
93
- micros = np.ndarray(
94
- nrows, dtype=np.uint16, buffer=data, offset=payloadoffset, strides=stride
95
- )
96
- payloadoffset += 2
97
- time = micros * _SECONDS_PER_TICK + seconds
98
- payloadtype = payloadtype & ~0x10
99
- if epoch is not None:
100
- time = epoch + pd.to_timedelta(time, "s") # type: ignore
101
- index = pd.Series(time)
102
- index.name = "Time"
103
-
104
- payloadsize = stride - payloadoffset - 1
105
- payloadtype = _payloadtypes[payloadtype]
106
- if dtype is not None and dtype != payloadtype:
107
- raise ValueError(f"expected payload type {dtype} but got {payloadtype}")
108
-
109
- elementsize = payloadtype.itemsize
110
- payloadshape = (nrows, payloadsize // elementsize)
111
- if length is not None and length != payloadshape[1]:
112
- raise ValueError(f"expected payload length {length} but got {payloadshape[1]}")
113
-
114
- payload = np.ndarray(
115
- payloadshape,
116
- dtype=payloadtype,
117
- buffer=data,
118
- offset=payloadoffset,
119
- strides=(stride, elementsize),
120
- )
121
-
122
- result = pd.DataFrame(payload, index=index, columns=columns)
123
- if keep_type:
124
- msgtype = np.ndarray(
125
- nrows, dtype=np.uint8, buffer=data, offset=0, strides=stride
126
- )
127
- msgtype = pd.Categorical.from_codes(msgtype, categories=_messagetypes) # type: ignore
128
- result[MessageType.__name__] = msgtype
129
- return result
@@ -1,55 +0,0 @@
1
- import pytest
2
- import numpy as np
3
- from contextlib import nullcontext
4
- from pytest import mark
5
- from harp.io import read, MessageType
6
- from tests.params import DataFileParam
7
-
8
- testdata = [
9
- DataFileParam(path="data/device_0.bin", expected_rows=1),
10
- DataFileParam(
11
- path="data/device_0.bin",
12
- expected_rows=1,
13
- expected_address=1, # actual address is 0
14
- expected_error=ValueError,
15
- ),
16
- DataFileParam(
17
- path="data/device_0.bin",
18
- expected_rows=1,
19
- expected_dtype=np.dtype("uint8"), # actual dtype is uint16
20
- expected_error=ValueError,
21
- ),
22
- DataFileParam(
23
- path="data/device_0.bin",
24
- expected_rows=1,
25
- expected_length=2, # actual length is 1
26
- expected_error=ValueError,
27
- ),
28
- DataFileParam(path="data/write_0.bin", expected_address=0, expected_rows=4),
29
- DataFileParam(
30
- path="data/write_0.bin", expected_address=0, expected_rows=4, keep_type=True
31
- ),
32
- ]
33
-
34
-
35
- @mark.parametrize("dataFile", testdata)
36
- def test_read(dataFile: DataFileParam):
37
- context = pytest.raises if dataFile.expected_error else nullcontext
38
- with context(dataFile.expected_error): # type: ignore
39
- data = read(
40
- dataFile.path,
41
- address=dataFile.expected_address,
42
- dtype=dataFile.expected_dtype,
43
- length=dataFile.expected_length,
44
- keep_type=dataFile.keep_type,
45
- )
46
- assert len(data) == dataFile.expected_rows
47
- if dataFile.keep_type:
48
- assert (
49
- MessageType.__name__ in data.columns
50
- and data[MessageType.__name__].dtype == "category"
51
- )
52
-
53
- if dataFile.expected_cols:
54
- for col in dataFile.expected_cols:
55
- assert col in data.columns
File without changes
File without changes
File without changes
File without changes
File without changes