harp-python 0.2.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 (28) hide show
  1. harp_python-0.3.0/.github/workflows/build.yml +37 -0
  2. {harp-python-0.2.0 → harp_python-0.3.0}/PKG-INFO +19 -3
  3. {harp-python-0.2.0 → harp_python-0.3.0}/README.md +13 -1
  4. {harp-python-0.2.0 → harp_python-0.3.0}/harp/__init__.py +2 -0
  5. {harp-python-0.2.0 → harp_python-0.3.0}/harp/io.py +8 -15
  6. {harp-python-0.2.0 → harp_python-0.3.0}/harp/model.py +6 -17
  7. {harp-python-0.2.0 → harp_python-0.3.0}/harp/reader.py +28 -14
  8. {harp-python-0.2.0 → harp_python-0.3.0}/harp/schema.py +9 -11
  9. {harp-python-0.2.0 → harp_python-0.3.0}/harp_python.egg-info/PKG-INFO +19 -3
  10. {harp-python-0.2.0 → harp_python-0.3.0}/harp_python.egg-info/SOURCES.txt +1 -0
  11. {harp-python-0.2.0 → harp_python-0.3.0}/harp_python.egg-info/requires.txt +5 -1
  12. {harp-python-0.2.0 → harp_python-0.3.0}/pyproject.toml +24 -14
  13. {harp-python-0.2.0 → harp_python-0.3.0}/tests/params.py +4 -2
  14. {harp-python-0.2.0 → harp_python-0.3.0}/tests/test_io.py +7 -10
  15. {harp-python-0.2.0 → harp_python-0.3.0}/tests/test_reader.py +1 -0
  16. {harp-python-0.2.0 → harp_python-0.3.0}/tests/test_schema.py +1 -0
  17. {harp-python-0.2.0 → harp_python-0.3.0}/.gitignore +0 -0
  18. {harp-python-0.2.0 → harp_python-0.3.0}/.gitmodules +0 -0
  19. {harp-python-0.2.0 → harp_python-0.3.0}/LICENSE +0 -0
  20. {harp-python-0.2.0 → harp_python-0.3.0}/MANIFEST.in +0 -0
  21. {harp-python-0.2.0 → harp_python-0.3.0}/harp/common.yml +0 -0
  22. {harp-python-0.2.0 → harp_python-0.3.0}/harp_python.egg-info/dependency_links.txt +0 -0
  23. {harp-python-0.2.0 → harp_python-0.3.0}/harp_python.egg-info/top_level.txt +0 -0
  24. {harp-python-0.2.0 → harp_python-0.3.0}/setup.cfg +0 -0
  25. {harp-python-0.2.0 → harp_python-0.3.0}/tests/__init__.py +0 -0
  26. {harp-python-0.2.0 → harp_python-0.3.0}/tests/data/device.yml +0 -0
  27. {harp-python-0.2.0 → harp_python-0.3.0}/tests/data/device_0.bin +0 -0
  28. {harp-python-0.2.0 → harp_python-0.3.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.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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: harp-python
3
- Version: 0.2.0
3
+ Version: 0.3.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,31 @@ 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"
28
32
 
29
33
  # harp-python
30
34
 
31
- A low-level interface to data collected with the [Harp](https://harp-tech.org/) binary protocol.
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).
32
48
 
33
49
  ## Data model
34
50
 
@@ -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
 
@@ -1,3 +1,5 @@
1
1
  from harp.io import REFERENCE_EPOCH, MessageType, read
2
2
  from harp.reader import create_reader
3
3
  from harp.schema import read_schema
4
+
5
+ __all__ = ["REFERENCE_EPOCH", "MessageType", "read", "create_reader", "read_schema"]
@@ -2,9 +2,10 @@ from datetime import datetime
2
2
  from enum import IntEnum
3
3
  from os import PathLike
4
4
  from typing import Any, BinaryIO, Optional, Union
5
- from pandas._typing import Axes
5
+
6
6
  import numpy as np
7
7
  import pandas as pd
8
+ from pandas._typing import Axes
8
9
 
9
10
  REFERENCE_EPOCH = datetime(1904, 1, 1)
10
11
  """The reference epoch for UTC harp time."""
@@ -73,29 +74,23 @@ def read(
73
74
  """
74
75
  data = np.fromfile(file, dtype=np.uint8)
75
76
  if len(data) == 0:
76
- return pd.DataFrame(
77
- columns=columns, index=pd.Index([], dtype=np.float64, name="Time")
78
- )
77
+ return pd.DataFrame(columns=columns, index=pd.Index([], dtype=np.float64, name="Time"))
79
78
 
80
79
  if address is not None and address != data[2]:
81
80
  raise ValueError(f"expected address {address} but got {data[2]}")
82
81
 
83
82
  index = None
84
- stride = data[1] + 2
83
+ stride = int(data[1] + 2)
85
84
  nrows = len(data) // stride
86
85
  payloadtype = data[4]
87
86
  payloadoffset = 5
88
87
  if payloadtype & 0x10 != 0:
89
- seconds = np.ndarray(
90
- nrows, dtype=np.uint32, buffer=data, offset=payloadoffset, strides=stride
91
- )
88
+ seconds = np.ndarray(nrows, dtype=np.uint32, buffer=data, offset=payloadoffset, strides=stride)
92
89
  payloadoffset += 4
93
- micros = np.ndarray(
94
- nrows, dtype=np.uint16, buffer=data, offset=payloadoffset, strides=stride
95
- )
90
+ micros = np.ndarray(nrows, dtype=np.uint16, buffer=data, offset=payloadoffset, strides=stride)
96
91
  payloadoffset += 2
97
92
  time = micros * _SECONDS_PER_TICK + seconds
98
- payloadtype = payloadtype & ~0x10
93
+ payloadtype = payloadtype & ~np.uint8(0x10)
99
94
  if epoch is not None:
100
95
  time = epoch + pd.to_timedelta(time, "s") # type: ignore
101
96
  index = pd.Series(time)
@@ -121,9 +116,7 @@ def read(
121
116
 
122
117
  result = pd.DataFrame(payload, index=index, columns=columns)
123
118
  if keep_type:
124
- msgtype = np.ndarray(
125
- nrows, dtype=np.uint8, buffer=data, offset=0, strides=stride
126
- )
119
+ msgtype = np.ndarray(nrows, dtype=np.uint8, buffer=data, offset=0, strides=stride)
127
120
  msgtype = pd.Categorical.from_codes(msgtype, categories=_messagetypes) # type: ignore
128
121
  result[MessageType.__name__] = msgtype
129
122
  return result
@@ -5,8 +5,7 @@
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
10
  from pydantic import (
12
11
  BaseModel,
@@ -136,16 +135,12 @@ class Visibility(Enum):
136
135
  class Register(BaseModel):
137
136
  address: Annotated[
138
137
  int,
139
- Field(
140
- le=255, description="Specifies the unique 8-bit address of the register."
141
- ),
138
+ Field(le=255, description="Specifies the unique 8-bit address of the register."),
142
139
  ]
143
140
  type: Annotated[PayloadType, BeforeValidator(lambda v: PayloadType[v])]
144
141
  length: Annotated[
145
142
  Optional[int],
146
- Field(
147
- ge=1, default=1, description="Specifies the length of the register payload."
148
- ),
143
+ Field(ge=1, default=1, description="Specifies the length of the register payload."),
149
144
  ]
150
145
  access: Union[Access, List[Access]] = Field(
151
146
  ..., description="Specifies the expected use of the register."
@@ -191,12 +186,6 @@ class Registers(BaseModel):
191
186
 
192
187
  class Model(Registers):
193
188
  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
- )
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.")
@@ -1,16 +1,19 @@
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 Any, BinaryIO, 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
15
18
 
16
19
 
@@ -27,8 +30,7 @@ class _ReadRegister(Protocol):
27
30
  file: Optional[Union[str, bytes, PathLike[Any], BinaryIO]] = None,
28
31
  epoch: Optional[datetime] = None,
29
32
  keep_type: bool = False,
30
- ) -> DataFrame:
31
- ...
33
+ ) -> DataFrame: ...
32
34
 
33
35
 
34
36
  class RegisterReader:
@@ -44,13 +46,27 @@ class RegisterReader:
44
46
  self.read = read
45
47
 
46
48
 
49
+ class RegisterMap(UserDict[str, RegisterReader]):
50
+ _address_map: Mapping[int, RegisterReader]
51
+
52
+ def __init__(self, registers: Mapping[str, RegisterReader]) -> None:
53
+ super().__init__(registers)
54
+ self._address_map = {value.register.address: value for value in registers.values()}
55
+
56
+ def __getitem__(self, key: Union[str, int]) -> RegisterReader:
57
+ if isinstance(key, int):
58
+ return self._address_map[key]
59
+ else:
60
+ return super().__getitem__(key)
61
+
62
+
47
63
  class DeviceReader:
48
64
  device: Model
49
- registers: dict[str, RegisterReader]
65
+ registers: RegisterMap
50
66
 
51
- def __init__(self, device: Model, registers: dict[str, RegisterReader]) -> None:
67
+ def __init__(self, device: Model, registers: Mapping[str, RegisterReader]) -> None:
52
68
  self.device = device
53
- self.registers = registers
69
+ self.registers = RegisterMap(registers)
54
70
 
55
71
  def __dir__(self) -> Iterable[str]:
56
72
  return self.registers.keys()
@@ -214,7 +230,7 @@ def create_reader(
214
230
  keep_type: bool = False,
215
231
  ):
216
232
  """Creates a device reader object from the specified dataset or schema.
217
-
233
+
218
234
  Parameters
219
235
  ----------
220
236
  device
@@ -249,9 +265,7 @@ def create_reader(
249
265
  base_path = path / device.device if is_dir else path.parent / device.device
250
266
 
251
267
  reg_readers = {
252
- name: _create_register_parser(
253
- device, name, _ReaderParams(base_path, epoch, keep_type)
254
- )
268
+ name: _create_register_parser(device, name, _ReaderParams(base_path, epoch, keep_type))
255
269
  for name in device.registers.keys()
256
270
  }
257
271
  return DeviceReader(device, reg_readers)
@@ -1,21 +1,21 @@
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:
9
11
  file = resources.files(__package__) / "common.yml"
10
- with file.open("rt") as fileIO:
12
+ with file.open("r") as fileIO:
11
13
  return parse_yaml_raw_as(Registers, fileIO.read())
12
14
 
13
15
 
14
- def read_schema(
15
- file: Union[str, PathLike, TextIO], include_common_registers: bool = True
16
- ) -> Model:
16
+ def read_schema(file: Union[str, PathLike, TextIO], include_common_registers: bool = True) -> Model:
17
17
  """Read and parse a device schema from the specified file.
18
-
18
+
19
19
  Parameters
20
20
  ----------
21
21
  file
@@ -35,14 +35,12 @@ def read_schema(
35
35
  return read_schema(fileIO)
36
36
  else:
37
37
  schema = parse_yaml_raw_as(Model, file.read())
38
- if not "WhoAmI" in schema.registers and include_common_registers:
38
+ if "WhoAmI" not in schema.registers and include_common_registers:
39
39
  common = _read_common_registers()
40
40
  schema.registers = dict(common.registers, **schema.registers)
41
41
  if common.bitMasks:
42
42
  schema.bitMasks = (
43
- common.bitMasks
44
- if schema.bitMasks is None
45
- else dict(common.bitMasks, **schema.bitMasks)
43
+ common.bitMasks if schema.bitMasks is None else dict(common.bitMasks, **schema.bitMasks)
46
44
  )
47
45
  if common.groupMasks:
48
46
  schema.groupMasks = (
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: harp-python
3
- Version: 0.2.0
3
+ Version: 0.3.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,31 @@ 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"
28
32
 
29
33
  # harp-python
30
34
 
31
- A low-level interface to data collected with the [Harp](https://harp-tech.org/) binary protocol.
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).
32
48
 
33
49
  ## Data model
34
50
 
@@ -4,6 +4,7 @@ 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
@@ -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,21 @@ 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.pyright]
71
+ venvPath = "."
72
+ venv = ".venv"
73
+ exclude = [
74
+ ".venv/*",
75
+ "reflex-generator"
76
+ ]
77
+
78
+ [tool.codespell]
79
+ skip = '.git,*.pdf,*.svg'
80
+ ignore-words-list = 'nd'
@@ -1,8 +1,10 @@
1
- import numpy as np
1
+ from dataclasses import dataclass
2
2
  from os import PathLike
3
3
  from pathlib import Path
4
- from dataclasses import dataclass
5
4
  from typing import Iterable, Optional, Type, Union
5
+
6
+ import numpy as np
7
+
6
8
  from harp.model import Model
7
9
 
8
10
  datapath = Path(__file__).parent
@@ -1,8 +1,10 @@
1
- import pytest
2
- import numpy as np
3
1
  from contextlib import nullcontext
2
+
3
+ import numpy as np
4
+ import pytest
4
5
  from pytest import mark
5
- from harp.io import read, MessageType
6
+
7
+ from harp.io import MessageType, read
6
8
  from tests.params import DataFileParam
7
9
 
8
10
  testdata = [
@@ -26,9 +28,7 @@ testdata = [
26
28
  expected_error=ValueError,
27
29
  ),
28
30
  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
- ),
31
+ DataFileParam(path="data/write_0.bin", expected_address=0, expected_rows=4, keep_type=True),
32
32
  ]
33
33
 
34
34
 
@@ -45,10 +45,7 @@ def test_read(dataFile: DataFileParam):
45
45
  )
46
46
  assert len(data) == dataFile.expected_rows
47
47
  if dataFile.keep_type:
48
- assert (
49
- MessageType.__name__ in data.columns
50
- and data[MessageType.__name__].dtype == "category"
51
- )
48
+ assert MessageType.__name__ in data.columns and data[MessageType.__name__].dtype == "category"
52
49
 
53
50
  if dataFile.expected_cols:
54
51
  for col in dataFile.expected_cols:
@@ -1,5 +1,6 @@
1
1
  import numpy as np
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
@@ -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
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes