harp-python 0.2.0__py3-none-any.whl → 0.4.0__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.
harp/__init__.py CHANGED
@@ -1,3 +1,5 @@
1
- from harp.io import REFERENCE_EPOCH, MessageType, read
1
+ from harp.io import REFERENCE_EPOCH, MessageType, read, to_buffer, to_file
2
2
  from harp.reader import create_reader
3
3
  from harp.schema import read_schema
4
+
5
+ __all__ = ["REFERENCE_EPOCH", "MessageType", "read", "to_buffer", "to_file", "create_reader", "read_schema"]
harp/io.py CHANGED
@@ -2,9 +2,13 @@ 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
+ import numpy.typing as npt
7
8
  import pandas as pd
9
+ from pandas._typing import Axes
10
+
11
+ from harp.typing import _BufferLike, _FileLike
8
12
 
9
13
  REFERENCE_EPOCH = datetime(1904, 1, 1)
10
14
  """The reference epoch for UTC harp time."""
@@ -20,8 +24,9 @@ class MessageType(IntEnum):
20
24
 
21
25
 
22
26
  _SECONDS_PER_TICK = 32e-6
27
+ _PAYLOAD_TIMESTAMP_MASK = 0x10
23
28
  _messagetypes = [type.name for type in MessageType]
24
- _payloadtypes = {
29
+ _dtypefrompayloadtype = {
25
30
  1: np.dtype(np.uint8),
26
31
  2: np.dtype(np.uint16),
27
32
  4: np.dtype(np.uint32),
@@ -32,10 +37,11 @@ _payloadtypes = {
32
37
  136: np.dtype(np.int64),
33
38
  68: np.dtype(np.float32),
34
39
  }
40
+ _payloadtypefromdtype = {v: k for k, v in _dtypefrompayloadtype.items()}
35
41
 
36
42
 
37
43
  def read(
38
- file: Union[str, bytes, PathLike[Any], BinaryIO],
44
+ file_or_buf: Union[_FileLike, _BufferLike],
39
45
  address: Optional[int] = None,
40
46
  dtype: Optional[np.dtype] = None,
41
47
  length: Optional[int] = None,
@@ -43,22 +49,22 @@ def read(
43
49
  epoch: Optional[datetime] = None,
44
50
  keep_type: bool = False,
45
51
  ):
46
- """Read single-register Harp data from the specified file.
52
+ """Read single-register Harp data from the specified file or buffer.
47
53
 
48
54
  Parameters
49
55
  ----------
50
- file
51
- Open file object or filename containing binary data from
56
+ file_or_buf
57
+ File path, open file object, or buffer containing binary data from
52
58
  a single device register.
53
59
  address
54
60
  Expected register address. If specified, the address of
55
- the first message in the file is used for validation.
61
+ the first message is used for validation.
56
62
  dtype
57
63
  Expected data type of the register payload. If specified, the
58
- payload type of the first message in the file is used for validation.
64
+ payload type of the first message is used for validation.
59
65
  length
60
66
  Expected number of elements in register payload. If specified, the
61
- payload length of the first message in the file is used for validation.
67
+ payload length of the first message is used for validation.
62
68
  columns
63
69
  The optional column labels to use for the data values.
64
70
  epoch
@@ -71,38 +77,43 @@ def read(
71
77
  -------
72
78
  A pandas data frame containing message data, sorted by time.
73
79
  """
74
- data = np.fromfile(file, dtype=np.uint8)
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
+
75
87
  if len(data) == 0:
76
88
  return pd.DataFrame(
77
- columns=columns, index=pd.Index([], dtype=np.float64, name="Time")
89
+ columns=columns,
90
+ index=pd.DatetimeIndex([], name="Time")
91
+ if epoch
92
+ else pd.Index([], dtype=np.float64, name="Time"),
78
93
  )
79
94
 
80
95
  if address is not None and address != data[2]:
81
96
  raise ValueError(f"expected address {address} but got {data[2]}")
82
97
 
83
98
  index = None
84
- stride = data[1] + 2
99
+ stride = int(data[1] + 2)
85
100
  nrows = len(data) // stride
86
101
  payloadtype = data[4]
87
102
  payloadoffset = 5
88
- if payloadtype & 0x10 != 0:
89
- seconds = np.ndarray(
90
- nrows, dtype=np.uint32, buffer=data, offset=payloadoffset, strides=stride
91
- )
103
+ if payloadtype & _PAYLOAD_TIMESTAMP_MASK != 0:
104
+ seconds = np.ndarray(nrows, dtype=np.uint32, buffer=data, offset=payloadoffset, strides=stride)
92
105
  payloadoffset += 4
93
- micros = np.ndarray(
94
- nrows, dtype=np.uint16, buffer=data, offset=payloadoffset, strides=stride
95
- )
106
+ micros = np.ndarray(nrows, dtype=np.uint16, buffer=data, offset=payloadoffset, strides=stride)
96
107
  payloadoffset += 2
97
108
  time = micros * _SECONDS_PER_TICK + seconds
98
- payloadtype = payloadtype & ~0x10
109
+ payloadtype = payloadtype & ~np.uint8(_PAYLOAD_TIMESTAMP_MASK)
99
110
  if epoch is not None:
100
111
  time = epoch + pd.to_timedelta(time, "s") # type: ignore
101
112
  index = pd.Series(time)
102
113
  index.name = "Time"
103
114
 
104
115
  payloadsize = stride - payloadoffset - 1
105
- payloadtype = _payloadtypes[payloadtype]
116
+ payloadtype = _dtypefrompayloadtype[payloadtype]
106
117
  if dtype is not None and dtype != payloadtype:
107
118
  raise ValueError(f"expected payload type {dtype} but got {payloadtype}")
108
119
 
@@ -121,9 +132,143 @@ def read(
121
132
 
122
133
  result = pd.DataFrame(payload, index=index, columns=columns)
123
134
  if keep_type:
124
- msgtype = np.ndarray(
125
- nrows, dtype=np.uint8, buffer=data, offset=0, strides=stride
126
- )
135
+ msgtype = np.ndarray(nrows, dtype=np.uint8, buffer=data, offset=0, strides=stride)
127
136
  msgtype = pd.Categorical.from_codes(msgtype, categories=_messagetypes) # type: ignore
128
137
  result[MessageType.__name__] = msgtype
129
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)
harp/model.py CHANGED
@@ -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.")
harp/reader.py CHANGED
@@ -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)
harp/schema.py CHANGED
@@ -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 = (
harp/typing.py ADDED
@@ -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
@@ -19,16 +19,33 @@ License-File: LICENSE
19
19
  Requires-Dist: pydantic-yaml
20
20
  Requires-Dist: pandas
21
21
  Provides-Extra: dev
22
- Requires-Dist: datamodel-code-generator ; extra == 'dev'
23
- Requires-Dist: pytest ; extra == 'dev'
24
- Requires-Dist: black ; 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"
25
29
  Provides-Extra: jupyter
26
- Requires-Dist: ipykernel ; extra == 'jupyter'
27
- Requires-Dist: matplotlib ; extra == 'jupyter'
30
+ Requires-Dist: ipykernel; extra == "jupyter"
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
 
@@ -0,0 +1,12 @@
1
+ harp/__init__.py,sha256=N6fclA4pCSdBwcJELnN15p5QioU_6F01gH-8F3wr_pI,264
2
+ harp/common.yml,sha256=qcejMFu00JjI5rkubuEDdrUu_uFkN17f_Ljb6yBd-RM,4452
3
+ harp/io.py,sha256=pJDLMRkIpbKjjSX-Le5ya_xJA6Sx3AeLb1UO08ghI_E,9783
4
+ harp/model.py,sha256=djJ7Ah6GliwDh1rXhTYoqXSDMUJ3UI9PAKFL-F6ooAg,5769
5
+ harp/reader.py,sha256=fkjQZYrLtJ_TQK2t3e8GcEISjvhXt5rlf8Oao-tIwzM,8794
6
+ harp/schema.py,sha256=9tNwjnec8WtRkV8P_oblauW7btOi_y1vWFC6Tw4EWt0,1929
7
+ harp/typing.py,sha256=MCjnRwj-K3vybLfp_dFFqf7MwmJ7PmYe0yD91bt8uEk,355
8
+ harp_python-0.4.0.dist-info/licenses/LICENSE,sha256=KxnuV3gNCp4WmLUC8DYWJvCKPdxMpwmWIzAYBYdqPrM,1104
9
+ harp_python-0.4.0.dist-info/METADATA,sha256=7Zxwebl6JmF-xNB5ARodVLj_WKtdsQvTTDAY1ZKZIl8,3213
10
+ harp_python-0.4.0.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
11
+ harp_python-0.4.0.dist-info/top_level.txt,sha256=ztK1iHjgwmfXqxHCpu-e2uHIwdmodywYEUaWSODMvyM,5
12
+ harp_python-0.4.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.41.3)
2
+ Generator: setuptools (77.0.3)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,11 +0,0 @@
1
- harp/__init__.py,sha256=7_D-5szHZTqSdy1lBiFIkg7_WkEjdn8vzMacxpDkkCE,132
2
- harp/common.yml,sha256=qcejMFu00JjI5rkubuEDdrUu_uFkN17f_Ljb6yBd-RM,4452
3
- harp/io.py,sha256=7Mdoiz7QCoNYV7R-xTR-zFYDhVY-9-gpuDckbmBPfPs,4258
4
- harp/model.py,sha256=8apjJu4no1yZ_s0h8ZvCF__z5qAc4qg3koHEBukg84Q,5931
5
- harp/reader.py,sha256=_8J5niscJ5AenZtrcYWvmmoWbxQAfO78E3Lk30azsjc,8060
6
- harp/schema.py,sha256=3Y8tuwXqO5LkJroVKU1TV5TznpCAqmR0G2Cod7NQoHQ,1867
7
- harp_python-0.2.0.dist-info/LICENSE,sha256=KxnuV3gNCp4WmLUC8DYWJvCKPdxMpwmWIzAYBYdqPrM,1104
8
- harp_python-0.2.0.dist-info/METADATA,sha256=atBivmIIVWmjjN_C11YnjZV6W7JoW9MmAnjvZuXe5MM,2547
9
- harp_python-0.2.0.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92
10
- harp_python-0.2.0.dist-info/top_level.txt,sha256=ztK1iHjgwmfXqxHCpu-e2uHIwdmodywYEUaWSODMvyM,5
11
- harp_python-0.2.0.dist-info/RECORD,,