harp-python 0.1.0__tar.gz → 0.3.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.3.0/.github/workflows/build.yml +37 -0
  2. {harp-python-0.1.0 → harp_python-0.3.0}/.gitignore +1 -1
  3. harp_python-0.3.0/PKG-INFO +102 -0
  4. harp_python-0.3.0/README.md +70 -0
  5. harp_python-0.3.0/harp/__init__.py +5 -0
  6. harp_python-0.3.0/harp/io.py +122 -0
  7. harp_python-0.3.0/harp/model.py +191 -0
  8. harp_python-0.3.0/harp/reader.py +271 -0
  9. harp_python-0.3.0/harp/schema.py +51 -0
  10. harp_python-0.3.0/harp_python.egg-info/PKG-INFO +102 -0
  11. {harp-python-0.1.0 → harp_python-0.3.0}/harp_python.egg-info/SOURCES.txt +11 -1
  12. {harp-python-0.1.0 → harp_python-0.3.0}/harp_python.egg-info/requires.txt +6 -1
  13. {harp-python-0.1.0 → harp_python-0.3.0}/pyproject.toml +25 -15
  14. harp_python-0.3.0/tests/__init__.py +0 -0
  15. harp_python-0.3.0/tests/data/device.yml +35 -0
  16. harp_python-0.3.0/tests/data/device_0.bin +0 -0
  17. harp_python-0.3.0/tests/data/write_0.bin +0 -0
  18. harp_python-0.3.0/tests/params.py +43 -0
  19. harp_python-0.3.0/tests/test_io.py +52 -0
  20. harp_python-0.3.0/tests/test_reader.py +33 -0
  21. harp_python-0.3.0/tests/test_schema.py +18 -0
  22. harp-python-0.1.0/PKG-INFO +0 -69
  23. harp-python-0.1.0/README.md +0 -42
  24. harp-python-0.1.0/harp/io.py +0 -58
  25. harp-python-0.1.0/harp/model.py +0 -184
  26. harp-python-0.1.0/harp/reader.py +0 -155
  27. harp-python-0.1.0/harp/schema.py +0 -31
  28. harp-python-0.1.0/harp_python.egg-info/PKG-INFO +0 -69
  29. {harp-python-0.1.0 → harp_python-0.3.0}/.gitmodules +0 -0
  30. {harp-python-0.1.0 → harp_python-0.3.0}/LICENSE +0 -0
  31. {harp-python-0.1.0 → harp_python-0.3.0}/MANIFEST.in +0 -0
  32. {harp-python-0.1.0 → harp_python-0.3.0}/harp/common.yml +0 -0
  33. {harp-python-0.1.0 → harp_python-0.3.0}/harp_python.egg-info/dependency_links.txt +0 -0
  34. {harp-python-0.1.0 → harp_python-0.3.0}/harp_python.egg-info/top_level.txt +0 -0
  35. {harp-python-0.1.0 → harp_python-0.3.0}/setup.cfg +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.11]
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
@@ -9,4 +9,4 @@ __pycache__
9
9
  dist
10
10
 
11
11
  # Data files
12
- data
12
+ /data
@@ -0,0 +1,102 @@
1
+ Metadata-Version: 2.1
2
+ Name: harp-python
3
+ Version: 0.3.0
4
+ Summary: A low-level interface for loading binary Harp protocol data
5
+ Author-email: Goncalo Lopes <g.lopes@neurogears.org>
6
+ License: MIT License
7
+ Project-URL: Bug Tracker, https://github.com/harp-tech/harp-python/issues
8
+ Project-URL: Source Code, https://github.com/harp-tech/harp-python
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Programming Language :: Python
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Requires-Python: >=3.9.0
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: pydantic-yaml
20
+ Requires-Dist: pandas
21
+ Provides-Extra: dev
22
+ Requires-Dist: datamodel-code-generator; extra == "dev"
23
+ Requires-Dist: pandas-stubs; extra == "dev"
24
+ Requires-Dist: pytest; 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"
29
+ Provides-Extra: jupyter
30
+ Requires-Dist: ipykernel; extra == "jupyter"
31
+ Requires-Dist: matplotlib; extra == "jupyter"
32
+
33
+ # harp-python
34
+
35
+ A low-level interface to data collected with the [Harp binary protocol](https://harp-tech.org/protocol/BinaryProtocol-8bit.html).
36
+
37
+ ## How to install
38
+
39
+ The source code is currently hosted on [GitHub](https://github.com/harp-tech/harp-python).
40
+
41
+ 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`.
42
+
43
+ ```sh
44
+ pip install harp-python
45
+ ```
46
+
47
+ The list of changes between each release can be found [here](https://github.com/harp-tech/harp-python/releases).
48
+
49
+ ## Data model
50
+
51
+ The interface makes use of a Pydantic data model generated from Harp device schema definitions. The schema data classes are used to automatically generate binary readers for each device.
52
+
53
+ All binary data files from a single device need to be stored in the same folder alongside the device meta-schema, named `device.yml`. Each register file should have the following naming convention `<deviceName>_<registerAddress>.bin`.
54
+
55
+ For example, for a dataset collected with a `Behavior` device, you might have:
56
+
57
+ ```
58
+ 📦device.harp
59
+ ┣ 📜Behavior_0.bin
60
+ ┣ 📜Behavior_1.bin
61
+ ...
62
+ ┗ 📜device.yml
63
+ ```
64
+
65
+ ## How to use
66
+
67
+ ### Create device reader object from schema
68
+
69
+ ```python
70
+ import harp
71
+ reader = harp.create_reader("device.harp")
72
+ ```
73
+
74
+ ### Read data from named register
75
+
76
+ ```python
77
+ reader.OperationControl.read()
78
+ ```
79
+
80
+ ### Access register metadata
81
+
82
+ ```python
83
+ reader.OperationControl.register.address
84
+ ```
85
+
86
+ ### Create device reader object with UTC datetime format
87
+
88
+ ```python
89
+ reader = harp.create_reader("device.harp", epoch=harp.REFERENCE_EPOCH)
90
+ ```
91
+
92
+ ### Read data with message type information
93
+
94
+ ```python
95
+ reader.OperationControl.read(keep_type=True)
96
+ ```
97
+
98
+ ### Read data from a specific file
99
+
100
+ ```python
101
+ reader.OperationControl.read("data/Behavior_10.bin")
102
+ ```
@@ -0,0 +1,70 @@
1
+ # harp-python
2
+
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).
16
+
17
+ ## Data model
18
+
19
+ The interface makes use of a Pydantic data model generated from Harp device schema definitions. The schema data classes are used to automatically generate binary readers for each device.
20
+
21
+ All binary data files from a single device need to be stored in the same folder alongside the device meta-schema, named `device.yml`. Each register file should have the following naming convention `<deviceName>_<registerAddress>.bin`.
22
+
23
+ For example, for a dataset collected with a `Behavior` device, you might have:
24
+
25
+ ```
26
+ 📦device.harp
27
+ ┣ 📜Behavior_0.bin
28
+ ┣ 📜Behavior_1.bin
29
+ ...
30
+ ┗ 📜device.yml
31
+ ```
32
+
33
+ ## How to use
34
+
35
+ ### Create device reader object from schema
36
+
37
+ ```python
38
+ import harp
39
+ reader = harp.create_reader("device.harp")
40
+ ```
41
+
42
+ ### Read data from named register
43
+
44
+ ```python
45
+ reader.OperationControl.read()
46
+ ```
47
+
48
+ ### Access register metadata
49
+
50
+ ```python
51
+ reader.OperationControl.register.address
52
+ ```
53
+
54
+ ### Create device reader object with UTC datetime format
55
+
56
+ ```python
57
+ reader = harp.create_reader("device.harp", epoch=harp.REFERENCE_EPOCH)
58
+ ```
59
+
60
+ ### Read data with message type information
61
+
62
+ ```python
63
+ reader.OperationControl.read(keep_type=True)
64
+ ```
65
+
66
+ ### Read data from a specific file
67
+
68
+ ```python
69
+ reader.OperationControl.read("data/Behavior_10.bin")
70
+ ```
@@ -0,0 +1,5 @@
1
+ from harp.io import REFERENCE_EPOCH, MessageType, read
2
+ from harp.reader import create_reader
3
+ from harp.schema import read_schema
4
+
5
+ __all__ = ["REFERENCE_EPOCH", "MessageType", "read", "create_reader", "read_schema"]
@@ -0,0 +1,122 @@
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 pandas as pd
8
+ from pandas._typing import Axes
9
+
10
+ REFERENCE_EPOCH = datetime(1904, 1, 1)
11
+ """The reference epoch for UTC harp time."""
12
+
13
+
14
+ class MessageType(IntEnum):
15
+ """Specifies the type of a Harp message."""
16
+
17
+ NA = 0
18
+ READ = 1
19
+ WRITE = 2
20
+ EVENT = 3
21
+
22
+
23
+ _SECONDS_PER_TICK = 32e-6
24
+ _messagetypes = [type.name for type in MessageType]
25
+ _payloadtypes = {
26
+ 1: np.dtype(np.uint8),
27
+ 2: np.dtype(np.uint16),
28
+ 4: np.dtype(np.uint32),
29
+ 8: np.dtype(np.uint64),
30
+ 129: np.dtype(np.int8),
31
+ 130: np.dtype(np.int16),
32
+ 132: np.dtype(np.int32),
33
+ 136: np.dtype(np.int64),
34
+ 68: np.dtype(np.float32),
35
+ }
36
+
37
+
38
+ def read(
39
+ file: Union[str, bytes, PathLike[Any], BinaryIO],
40
+ address: Optional[int] = None,
41
+ dtype: Optional[np.dtype] = None,
42
+ length: Optional[int] = None,
43
+ columns: Optional[Axes] = None,
44
+ epoch: Optional[datetime] = None,
45
+ keep_type: bool = False,
46
+ ):
47
+ """Read single-register Harp data from the specified file.
48
+
49
+ Parameters
50
+ ----------
51
+ file
52
+ Open file object or filename containing binary data from
53
+ a single device register.
54
+ address
55
+ Expected register address. If specified, the address of
56
+ the first message in the file is used for validation.
57
+ dtype
58
+ Expected data type of the register payload. If specified, the
59
+ payload type of the first message in the file is used for validation.
60
+ length
61
+ Expected number of elements in register payload. If specified, the
62
+ payload length of the first message in the file is used for validation.
63
+ columns
64
+ The optional column labels to use for the data values.
65
+ epoch
66
+ Reference datetime at which time zero begins. If specified,
67
+ the result data frame will have a datetime index.
68
+ keep_type
69
+ Specifies whether to include a column with the message type.
70
+
71
+ Returns
72
+ -------
73
+ A pandas data frame containing message data, sorted by time.
74
+ """
75
+ data = np.fromfile(file, dtype=np.uint8)
76
+ if len(data) == 0:
77
+ return pd.DataFrame(columns=columns, index=pd.Index([], dtype=np.float64, name="Time"))
78
+
79
+ if address is not None and address != data[2]:
80
+ raise ValueError(f"expected address {address} but got {data[2]}")
81
+
82
+ index = None
83
+ stride = int(data[1] + 2)
84
+ nrows = len(data) // stride
85
+ payloadtype = data[4]
86
+ payloadoffset = 5
87
+ if payloadtype & 0x10 != 0:
88
+ seconds = np.ndarray(nrows, dtype=np.uint32, buffer=data, offset=payloadoffset, strides=stride)
89
+ payloadoffset += 4
90
+ micros = np.ndarray(nrows, dtype=np.uint16, buffer=data, offset=payloadoffset, strides=stride)
91
+ payloadoffset += 2
92
+ time = micros * _SECONDS_PER_TICK + seconds
93
+ payloadtype = payloadtype & ~np.uint8(0x10)
94
+ if epoch is not None:
95
+ time = epoch + pd.to_timedelta(time, "s") # type: ignore
96
+ index = pd.Series(time)
97
+ index.name = "Time"
98
+
99
+ payloadsize = stride - payloadoffset - 1
100
+ payloadtype = _payloadtypes[payloadtype]
101
+ if dtype is not None and dtype != payloadtype:
102
+ raise ValueError(f"expected payload type {dtype} but got {payloadtype}")
103
+
104
+ elementsize = payloadtype.itemsize
105
+ payloadshape = (nrows, payloadsize // elementsize)
106
+ if length is not None and length != payloadshape[1]:
107
+ raise ValueError(f"expected payload length {length} but got {payloadshape[1]}")
108
+
109
+ payload = np.ndarray(
110
+ payloadshape,
111
+ dtype=payloadtype,
112
+ buffer=data,
113
+ offset=payloadoffset,
114
+ strides=(stride, elementsize),
115
+ )
116
+
117
+ result = pd.DataFrame(payload, index=index, columns=columns)
118
+ if keep_type:
119
+ msgtype = np.ndarray(nrows, dtype=np.uint8, buffer=data, offset=0, strides=stride)
120
+ msgtype = pd.Categorical.from_codes(msgtype, categories=_messagetypes) # type: ignore
121
+ result[MessageType.__name__] = msgtype
122
+ return result
@@ -0,0 +1,191 @@
1
+ # generated by datamodel-codegen:
2
+ # filename: device.json
3
+ # timestamp: 2023-10-30T11:46:57+00:00
4
+
5
+ from __future__ import annotations
6
+
7
+ from enum import Enum
8
+ from typing import Annotated, Dict, List, Optional, Union
9
+
10
+ from pydantic import (
11
+ BaseModel,
12
+ BeforeValidator,
13
+ ConfigDict,
14
+ Field,
15
+ RootModel,
16
+ field_serializer,
17
+ )
18
+
19
+
20
+ class PayloadType(str, Enum):
21
+ U8 = "uint8"
22
+ S8 = "int8"
23
+ U16 = "uint16"
24
+ S16 = "int16"
25
+ U32 = "uint32"
26
+ S32 = "int32"
27
+ U64 = "uint64"
28
+ S64 = "int64"
29
+ Float = "float32"
30
+
31
+
32
+ class Access(Enum):
33
+ Read = "Read"
34
+ Write = "Write"
35
+ Event = "Event"
36
+
37
+
38
+ class MaskValueItem(BaseModel):
39
+ model_config = ConfigDict(
40
+ extra="forbid",
41
+ )
42
+ value: int = Field(..., description="Specifies the numerical mask value.")
43
+ description: Optional[str] = Field(
44
+ None, description="Specifies a summary description of the mask value function."
45
+ )
46
+
47
+ def __int__(self):
48
+ return self.value
49
+
50
+
51
+ class MaskValue(RootModel[Union[int, MaskValueItem]]):
52
+ root: Union[int, MaskValueItem]
53
+
54
+
55
+ class BitMask(BaseModel):
56
+ description: Optional[str] = Field(
57
+ None, description="Specifies a summary description of the bit mask function."
58
+ )
59
+ bits: Dict[str, MaskValue]
60
+
61
+
62
+ class GroupMask(BaseModel):
63
+ description: Optional[str] = Field(
64
+ None, description="Specifies a summary description of the group mask function."
65
+ )
66
+ values: Dict[str, MaskValue]
67
+
68
+
69
+ class MaskType(RootModel[str]):
70
+ root: str = Field(
71
+ ...,
72
+ description="Specifies the name of the bit mask or group mask used to represent the payload value.",
73
+ )
74
+
75
+
76
+ class InterfaceType(RootModel[str]):
77
+ root: str = Field(
78
+ ...,
79
+ description="Specifies the name of the type used to represent the payload value in the high-level interface.",
80
+ )
81
+
82
+
83
+ class Converter(Enum):
84
+ None_ = "None"
85
+ Payload = "Payload"
86
+ RawPayload = "RawPayload"
87
+
88
+
89
+ class MinValue(RootModel[float]):
90
+ root: float = Field(
91
+ ...,
92
+ description="Specifies the minimum allowable value for the payload or payload member.",
93
+ )
94
+
95
+
96
+ class MaxValue(RootModel[float]):
97
+ root: float = Field(
98
+ ...,
99
+ description="Specifies the maximum allowable value for the payload or payload member.",
100
+ )
101
+
102
+
103
+ class DefaultValue(RootModel[float]):
104
+ root: float = Field(
105
+ ...,
106
+ description="Specifies the default value for the payload or payload member.",
107
+ )
108
+
109
+
110
+ class PayloadMember(BaseModel):
111
+ mask: Optional[int] = Field(
112
+ None,
113
+ description="Specifies the mask used to read and write this payload member.",
114
+ )
115
+ offset: Optional[int] = Field(
116
+ None,
117
+ description="Specifies the payload array offset where this payload member is stored.",
118
+ )
119
+ description: Optional[str] = Field(
120
+ None, description="Specifies a summary description of the payload member."
121
+ )
122
+ minValue: Optional[MinValue] = None
123
+ maxValue: Optional[MaxValue] = None
124
+ defaultValue: Optional[DefaultValue] = None
125
+ maskType: Optional[MaskType] = None
126
+ interfaceType: Optional[InterfaceType] = None
127
+ converter: Optional[Converter] = None
128
+
129
+
130
+ class Visibility(Enum):
131
+ public = "public"
132
+ private = "private"
133
+
134
+
135
+ class Register(BaseModel):
136
+ address: Annotated[
137
+ int,
138
+ Field(le=255, description="Specifies the unique 8-bit address of the register."),
139
+ ]
140
+ type: Annotated[PayloadType, BeforeValidator(lambda v: PayloadType[v])]
141
+ length: Annotated[
142
+ Optional[int],
143
+ Field(ge=1, default=1, description="Specifies the length of the register payload."),
144
+ ]
145
+ access: Union[Access, List[Access]] = Field(
146
+ ..., description="Specifies the expected use of the register."
147
+ )
148
+ description: Optional[str] = Field(
149
+ None, description="Specifies a summary description of the register function."
150
+ )
151
+ minValue: Optional[MinValue] = None
152
+ maxValue: Optional[MaxValue] = None
153
+ defaultValue: Optional[DefaultValue] = None
154
+ maskType: Optional[MaskType] = None
155
+ visibility: Optional[Visibility] = Field(
156
+ None,
157
+ description="Specifies whether the register function is exposed in the high-level interface.",
158
+ )
159
+ volatile: Optional[bool] = Field(
160
+ None,
161
+ description="Specifies whether register values can be saved in non-volatile memory.",
162
+ )
163
+ payloadSpec: Optional[Dict[str, PayloadMember]] = None
164
+ interfaceType: Optional[InterfaceType] = None
165
+ converter: Optional[Converter] = None
166
+
167
+ @field_serializer("type")
168
+ def _serialize_type(self, type: PayloadType):
169
+ return type.name
170
+
171
+
172
+ class Registers(BaseModel):
173
+ registers: Dict[str, Register] = Field(
174
+ ...,
175
+ description="Specifies the collection of registers implementing the device function.",
176
+ )
177
+ bitMasks: Optional[Dict[str, BitMask]] = Field(
178
+ None,
179
+ description="Specifies the collection of masks available to be used with the different registers.",
180
+ )
181
+ groupMasks: Optional[Dict[str, GroupMask]] = Field(
182
+ None,
183
+ description="Specifies the collection of group masks available to be used with the different registers.",
184
+ )
185
+
186
+
187
+ class Model(Registers):
188
+ device: str = Field(..., description="Specifies the name of the device.")
189
+ whoAmI: int = Field(..., description="Specifies the unique identifier for this device type.")
190
+ firmwareVersion: str = Field(..., description="Specifies the semantic version of the device firmware.")
191
+ hardwareTargets: str = Field(..., description="Specifies the semantic version of the device hardware.")