Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
b64f17f
Add new constructor methods
bruno-f-cruz May 9, 2024
ebd1956
Deprecate function
bruno-f-cruz May 9, 2024
2e51fc4
Document methods
bruno-f-cruz May 9, 2024
24b8977
Linting
bruno-f-cruz May 9, 2024
9c8bb7d
Add new constructor methods
bruno-f-cruz May 9, 2024
a890991
Deprecate function
bruno-f-cruz May 9, 2024
1b3d438
Document methods
bruno-f-cruz May 9, 2024
a76c6f0
Linting
bruno-f-cruz May 9, 2024
dbdbaa3
Merge branch 'feat-device-reader-constructors' of https://github.com/…
bruno-f-cruz Dec 13, 2024
828cd3b
Fix rebasing
bruno-f-cruz Dec 13, 2024
13a6862
Add deprecated decorator
bruno-f-cruz Dec 13, 2024
3cefa8e
Fix rebasing
bruno-f-cruz Dec 13, 2024
89870d9
Favor library's deprecated decorator
bruno-f-cruz Dec 13, 2024
83397d1
Linting
bruno-f-cruz Dec 13, 2024
48dadeb
Add requests as dependency
bruno-f-cruz Dec 13, 2024
4c39bd8
Move decorator to separate private helper module to prevent circular …
bruno-f-cruz Dec 13, 2024
132cb73
Ignore warnings emitted by deprecated decorator
bruno-f-cruz Dec 13, 2024
59258e7
Merge branch 'main' into feat-device-reader-constructors
bruno-f-cruz Mar 22, 2025
30ecc6b
Favor stdlib decorator
bruno-f-cruz Mar 22, 2025
c09c573
Refactor to #40
bruno-f-cruz Mar 22, 2025
cc258c1
Linting
bruno-f-cruz Mar 22, 2025
02059bf
Make `data` input to `_compose_parser` optional
bruno-f-cruz Apr 1, 2025
6a882c2
Add unittest for registers with payload spec
bruno-f-cruz Apr 1, 2025
3ee94ae
Suppress `pyright` error on internal method import
bruno-f-cruz Apr 1, 2025
f6f4c53
Merge pull request #46 from harp-tech/fix-parser
glopesdev Apr 2, 2025
204b225
Replace license classifier with SPDX expression
glopesdev Apr 2, 2025
3d97d0b
Merge pull request #47 from harp-tech/license-spdx
glopesdev Apr 2, 2025
edd9415
Add new constructor methods
bruno-f-cruz May 9, 2024
7662754
Deprecate function
bruno-f-cruz May 9, 2024
ceaa56d
Document methods
bruno-f-cruz May 9, 2024
7c8e5b4
Linting
bruno-f-cruz May 9, 2024
b5160a8
Add deprecated decorator
bruno-f-cruz Dec 13, 2024
58f3a0b
Linting
bruno-f-cruz Dec 13, 2024
a9cea29
Add requests as dependency
bruno-f-cruz Dec 13, 2024
b4abcaa
Move decorator to separate private helper module to prevent circular …
bruno-f-cruz Dec 13, 2024
e00c784
Ignore warnings emitted by deprecated decorator
bruno-f-cruz Dec 13, 2024
5a2b409
Favor stdlib decorator
bruno-f-cruz Mar 22, 2025
70cea11
Refactor to #40
bruno-f-cruz Mar 22, 2025
6ad34ec
Merge branch 'feat-device-reader-constructors' of https://github.com/…
bruno-f-cruz Jun 5, 2025
d063806
Linting
bruno-f-cruz Jun 5, 2025
724bca1
Import deprecated from typing_extensions
bruno-f-cruz Jun 5, 2025
7a4a81a
Ensure device_name is appended to base_path
bruno-f-cruz Jun 18, 2025
1e075ee
Add unittest
bruno-f-cruz Jun 18, 2025
c4db137
Add string type hinting to paths
bruno-f-cruz Jun 18, 2025
f6843d2
Refactor unions to legacy syntax
bruno-f-cruz Jun 18, 2025
b1c522e
Ensure request errors are raised from reader
bruno-f-cruz Jun 18, 2025
3dfa3e6
Shorten deprecation warning
bruno-f-cruz Jun 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion harp/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import numpy as np
import numpy.typing as npt
import pandas as pd
from pandas._typing import Axes
from pandas._typing import Axes # pyright: ignore[reportPrivateImportUsage]

from harp.typing import _BufferLike, _FileLike

Expand Down
240 changes: 238 additions & 2 deletions harp/reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@
from dataclasses import dataclass
from datetime import datetime
from functools import partial
from io import StringIO
from math import log2
from os import PathLike
from pathlib import Path
from typing import Callable, Iterable, Mapping, Optional, Protocol, Union

import requests
from numpy import dtype
from pandas import DataFrame, Series
from pandas._typing import Axes
from pandas._typing import Axes # pyright: ignore[reportPrivateImportUsage]
from typing_extensions import deprecated

from harp.io import MessageType, read
from harp.model import BitMask, GroupMask, Model, PayloadMember, Register
Expand Down Expand Up @@ -75,14 +78,246 @@ def __dir__(self) -> Iterable[str]:
def __getattr__(self, __name: str) -> RegisterReader:
return self.registers[__name]

@classmethod
def from_file(
cls,
filepath: Union[PathLike, str],
base_path: Optional[Union[PathLike, str]] = None,
include_common_registers: bool = True,
epoch: Optional[datetime] = None,
keep_type: bool = False,
) -> "DeviceReader":
"""Creates a device reader object from the specified schema yml file.

Parameters
----------
filepath
A path to the device yml schema describing the device.
base_path
The path to attempt to resolve the location of data files.
include_common_registers
Specifies whether to include the set of Harp common registers in the
parsed device schema object. If a parsed device schema object is provided,
this parameter is ignored.
epoch
The default reference datetime at which time zero begins. If specified,
the data frames returned by each register reader will have a datetime index.
keep_type
Specifies whether to include a column with the message type by default.

Returns
-------
A device reader object which can be used to read binary data for each
register or to access metadata about each register. Individual registers
can be accessed using dot notation using the name of the register as the
key.
"""

device = read_schema(filepath, include_common_registers)
if base_path is None:
path = Path(filepath).absolute().resolve()
base_path = path.parent / device.device
else:
base_path = Path(base_path).absolute().resolve() / device.device

reg_readers = {
name: _create_register_handler(device, name, _ReaderParams(base_path, epoch, keep_type))
for name in device.registers.keys()
}
return cls(device, reg_readers)

@classmethod
def from_url(
cls,
url: str,
base_path: Optional[Union[PathLike, str]] = None,
include_common_registers: bool = True,
epoch: Optional[datetime] = None,
keep_type: bool = False,
timeout: int = 5,
) -> "DeviceReader":
"""Creates a device reader object from a url pointing to a device.yml file.

Parameters
----------
url
The url pointing to the device.yml schema describing the device.
base_path
The path to attempt to resolve the location of data files.
include_common_registers
Specifies whether to include the set of Harp common registers in the
parsed device schema object. If a parsed device schema object is provided,
this parameter is ignored.
epoch
The default reference datetime at which time zero begins. If specified,
the data frames returned by each register reader will have a datetime index.
keep_type
Specifies whether to include a column with the message type by default.
timeout
The number of seconds to wait for the server to send data before giving up.
Returns
-------
A device reader object which can be used to read binary data for each
register or to access metadata about each register. Individual registers
can be accessed using dot notation using the name of the register as the
key.
"""

response = requests.get(url, timeout=timeout)
response.raise_for_status()
text = response.text

return cls.from_str(
text,
base_path=base_path,
include_common_registers=include_common_registers,
epoch=epoch,
keep_type=keep_type,
)

@classmethod
def from_str(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still looking for a better name:

  • from_yaml: may be misleading since the others also ultimately use YAML
  • from_schema: maybe a little bit better

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from_schema makes me nervous as people might think you should pass the actuall schema file (i.e. .yml). Would from_string be better (ToString is also a thing after all haha)?

cls,
schema: str,
base_path: Optional[Union[PathLike, str]] = None,
include_common_registers: bool = True,
epoch: Optional[datetime] = None,
keep_type: bool = False,
) -> "DeviceReader":
"""Creates a device reader object from a string containing a device.yml schema.

Parameters
----------
schema
The string containing the device.yml schema describing the device.
base_path
The path to attempt to resolve the location of data files.
include_common_registers
Specifies whether to include the set of Harp common registers in the
parsed device schema object. If a parsed device schema object is provided,
this parameter is ignored.
epoch
The default reference datetime at which time zero begins. If specified,
the data frames returned by each register reader will have a datetime index.
keep_type
Specifies whether to include a column with the message type by default.

Returns
-------
A device reader object which can be used to read binary data for each
register or to access metadata about each register. Individual registers
can be accessed using dot notation using the name of the register as the
key.
"""
device = read_schema(StringIO(schema), include_common_registers)
if base_path is None:
base_path = Path(device.device).absolute().resolve()
else:
base_path = Path(base_path).absolute().resolve()
base_path = base_path / device.device

reg_readers = {
name: _create_register_handler(device, name, _ReaderParams(base_path, epoch, keep_type))
for name in device.registers.keys()
}
return cls(device, reg_readers)

@classmethod
def from_model(
cls,
model: Model,
base_path: Optional[Union[PathLike, str]] = None,
epoch: Optional[datetime] = None,
keep_type: bool = False,
) -> "DeviceReader":
"""Creates a device reader object from a parsed device schema object.

Parameters
----------
model
The parsed device schema object describing the device.
base_path
The path to attempt to resolve the location of data files.
epoch
The default reference datetime at which time zero begins. If specified,
the data frames returned by each register reader will have a datetime index.
keep_type
Specifies whether to include a column with the message type by default.

Returns
-------
A device reader object which can be used to read binary data for each
register or to access metadata about each register. Individual registers
can be accessed using dot notation using the name of the register as the
key.
"""

if base_path is None:
base_path = Path(model.device).absolute().resolve()
else:
base_path = Path(base_path).absolute().resolve()
base_path = base_path / model.device

reg_readers = {
name: _create_register_handler(model, name, _ReaderParams(base_path, epoch, keep_type))
for name in model.registers.keys()
}
return cls(model, reg_readers)

@classmethod
def from_dataset(
cls,
dataset: Union[PathLike, str],
include_common_registers: bool = True,
epoch: Optional[datetime] = None,
keep_type: bool = False,
) -> "DeviceReader":
"""Creates a device reader object from the specified dataset folder.

Parameters
----------
dataset
A path to the dataset folder containing a device.yml schema describing the device.
include_common_registers
Specifies whether to include the set of Harp common registers in the
parsed device schema object. If a parsed device schema object is provided,
this parameter is ignored.
epoch
The default reference datetime at which time zero begins. If specified,
the data frames returned by each register reader will have a datetime index.
keep_type
Specifies whether to include a column with the message type by default.

Returns
-------
A device reader object which can be used to read binary data for each
register or to access metadata about each register. Individual registers
can be accessed using dot notation using the name of the register as the
key.
"""

path = Path(dataset).absolute().resolve()
is_dir = os.path.isdir(path)
if is_dir:
filepath = path / "device.yml"
return cls.from_file(
filepath=filepath,
base_path=path,
include_common_registers=include_common_registers,
epoch=epoch,
keep_type=keep_type,
)
else:
raise ValueError("The dataset must be a directory containing a device.yml file.")


def _compose_parser(
f: Callable[[DataFrame], DataFrame],
g: Callable[..., DataFrame],
params: _ReaderParams,
) -> Callable[..., DataFrame]:
def parser(
data,
data: Optional[Union[_FileLike, _BufferLike]] = None,
columns: Optional[Axes] = None,
epoch: Optional[datetime] = params.epoch,
keep_type: bool = params.keep_type,
Expand Down Expand Up @@ -229,6 +464,7 @@ def payload_parser(df: DataFrame):
return RegisterReader(register, reader)


@deprecated("This function is deprecated. Use DeviceReader.from_* methods instead.")
def create_reader(
device: Union[str, PathLike, Model],
include_common_registers: bool = True,
Expand Down
8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ description = "A low-level interface for loading binary Harp protocol data"
readme = "README.md"
requires-python = ">=3.9.0"
dynamic = ["version"]
license = {text = "MIT License"}
license = "MIT"

dependencies = [
"pydantic-yaml",
"pandas"
"pandas",
"requests"
]

classifiers = [
Expand All @@ -20,8 +21,7 @@ classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Operating System :: OS Independent",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent"
]

[project.urls]
Expand Down
16 changes: 16 additions & 0 deletions tests/data/device.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,22 @@ registers:
length: 3
access: Event
description: Reports the current values of the analog input lines.
AnalogDataPayloadSpec:
address: 44
type: S16
length: 3
access: Event
description: Reports the current values of the analog input lines.
payloadSpec:
AnalogInput0:
offset: 0
description: The voltage at the output of the ADC channel 0.
Encoder:
offset: 1
description: The quadrature counter value on Port 2
AnalogInput1:
offset: 2
description: The voltage at the output of the ADC channel 1.
bitMasks:
DigitalInputs:
description: Specifies the state of the digital input lines.
Expand Down
Loading