From b64f17ffc9186af858fa18136008bd7b63e35525 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Thu, 9 May 2024 08:35:57 -0700 Subject: [PATCH 01/42] Add new constructor methods --- harp/reader.py | 112 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/harp/reader.py b/harp/reader.py index 6b3645c..a222c3a 100644 --- a/harp/reader.py +++ b/harp/reader.py @@ -13,6 +13,7 @@ from harp.model import BitMask, GroupMask, Model, PayloadMember, Register from harp.io import MessageType, read from harp.schema import read_schema +import requests @dataclass @@ -75,6 +76,117 @@ def __dir__(self) -> Iterable[str]: def __getattr__(self, __name: str) -> RegisterReader: return self.registers[__name] + @staticmethod + def from_file( + filepath: PathLike, + base_path: Optional[PathLike] = None, + include_common_registers: bool = True, + epoch: Optional[datetime] = None, + keep_type: bool = False) -> "DeviceReader": + + 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_parser( + device, name, _ReaderParams(base_path, epoch, keep_type) + ) + for name in device.registers.keys() + } + return DeviceReader(device, reg_readers) + + @staticmethod + def from_url( + url: str, + base_path: Optional[PathLike] = None, + include_common_registers: bool = True, + epoch: Optional[datetime] = None, + keep_type: bool = False, + timeout: int = 5) -> "DeviceReader": + + response = requests.get(url, timeout=timeout) + text = response.text + + device = read_schema(text, include_common_registers) + if base_path is None: + base_path = Path(device.device).absolute().resolve() + else: + base_path = Path(base_path).absolute().resolve() + + reg_readers = { + name: _create_register_parser( + device, name, _ReaderParams(base_path, epoch, keep_type) + ) + for name in device.registers.keys() + } + return DeviceReader(device, reg_readers) + + @staticmethod + def from_str( + schema: str, + base_path: Optional[PathLike] = None, + include_common_registers: bool = True, + epoch: Optional[datetime] = None, + keep_type: bool = False) -> "DeviceReader": + + device = read_schema(schema, include_common_registers) + if base_path is None: + base_path = Path(device.device).absolute().resolve() + else: + base_path = Path(base_path).absolute().resolve() + + reg_readers = { + name: _create_register_parser( + device, name, _ReaderParams(base_path, epoch, keep_type) + ) + for name in device.registers.keys() + } + return DeviceReader(device, reg_readers) + + @staticmethod + def from_model( + model: Model, + base_path: Optional[PathLike] = None, + epoch: Optional[datetime] = None, + keep_type: bool = False) -> "DeviceReader": + + if base_path is None: + base_path = Path(model.device).absolute().resolve() + else: + base_path = Path(base_path).absolute().resolve() + + reg_readers = { + name: _create_register_parser( + model, name, _ReaderParams(base_path, epoch, keep_type) + ) + for name in model.registers.keys() + } + return DeviceReader(model, reg_readers) + + @staticmethod + def from_dataset( + dataset: PathLike, + include_common_registers: bool = True, + epoch: Optional[datetime] = None, + keep_type: bool = False) -> "DeviceReader": + + path = Path(dataset).absolute().resolve() + is_dir = os.path.isdir(path) + if is_dir: + filepath = path / "device.yml" + return DeviceReader.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], From ebd1956cd85889f2fe0e871fe1f2e151abf872ff Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Thu, 9 May 2024 08:44:56 -0700 Subject: [PATCH 02/42] Deprecate function --- harp/reader.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/harp/reader.py b/harp/reader.py index a222c3a..f7e9eba 100644 --- a/harp/reader.py +++ b/harp/reader.py @@ -1,4 +1,6 @@ import os +import requests +from deprecated import deprecated from math import log2 from os import PathLike from pathlib import Path @@ -10,10 +12,10 @@ from typing import Any, BinaryIO, Callable, Iterable, Mapping, Optional, Protocol, Union from collections import UserDict from pandas._typing import Axes +from harp import __version__ from harp.model import BitMask, GroupMask, Model, PayloadMember, Register from harp.io import MessageType, read from harp.schema import read_schema -import requests @dataclass @@ -335,7 +337,7 @@ def parser(df: DataFrame): reader = partial(reader, columns=[name]) return RegisterReader(register, reader) - +@deprecated("This function is deprecated. Use DeviceReader.from_file, DeviceReader.from_url, DeviceReader.from_str, and DeviceReader.from_model instead.") def create_reader( device: Union[str, PathLike, Model], include_common_registers: bool = True, From 2e51fc4748247c2aae218dd1b9f945af11b737b5 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Thu, 9 May 2024 08:55:49 -0700 Subject: [PATCH 03/42] Document methods --- harp/reader.py | 173 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 149 insertions(+), 24 deletions(-) diff --git a/harp/reader.py b/harp/reader.py index f7e9eba..91726a0 100644 --- a/harp/reader.py +++ b/harp/reader.py @@ -80,11 +80,37 @@ def __getattr__(self, __name: str) -> RegisterReader: @staticmethod def from_file( - filepath: PathLike, - base_path: Optional[PathLike] = None, - include_common_registers: bool = True, - epoch: Optional[datetime] = None, - keep_type: bool = False) -> "DeviceReader": + filepath: PathLike, + base_path: Optional[PathLike] = 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: @@ -103,12 +129,39 @@ def from_file( @staticmethod def from_url( - url: str, - base_path: Optional[PathLike] = None, - include_common_registers: bool = True, - epoch: Optional[datetime] = None, - keep_type: bool = False, - timeout: int = 5) -> "DeviceReader": + url: str, + base_path: Optional[PathLike] = 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) text = response.text @@ -129,11 +182,37 @@ def from_url( @staticmethod def from_str( - schema: str, - base_path: Optional[PathLike] = None, - include_common_registers: bool = True, - epoch: Optional[datetime] = None, - keep_type: bool = False) -> "DeviceReader": + schema: str, + base_path: Optional[PathLike] = 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(schema, include_common_registers) if base_path is None: @@ -151,10 +230,32 @@ def from_str( @staticmethod def from_model( - model: Model, - base_path: Optional[PathLike] = None, - epoch: Optional[datetime] = None, - keep_type: bool = False) -> "DeviceReader": + model: Model, + base_path: Optional[PathLike] = 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() @@ -171,10 +272,34 @@ def from_model( @staticmethod def from_dataset( - dataset: PathLike, - include_common_registers: bool = True, - epoch: Optional[datetime] = None, - keep_type: bool = False) -> "DeviceReader": + dataset: PathLike, + 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) From 24b89773fdeace6fe4fa9ff01292c620eddb9b31 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Thu, 9 May 2024 08:55:57 -0700 Subject: [PATCH 04/42] Linting --- harp/io.py | 3 ++- harp/model.py | 12 +++--------- harp/reader.py | 34 +++++++++++++++++++++------------- harp/schema.py | 6 ++++-- 4 files changed, 30 insertions(+), 25 deletions(-) diff --git a/harp/io.py b/harp/io.py index 6c9630b..a37270e 100644 --- a/harp/io.py +++ b/harp/io.py @@ -2,9 +2,10 @@ from enum import IntEnum from os import PathLike from typing import Any, BinaryIO, Optional, Union -from pandas._typing import Axes + import numpy as np import pandas as pd +from pandas._typing import Axes REFERENCE_EPOCH = datetime(1904, 1, 1) """The reference epoch for UTC harp time.""" diff --git a/harp/model.py b/harp/model.py index 7c66f1d..f28a7be 100644 --- a/harp/model.py +++ b/harp/model.py @@ -6,16 +6,10 @@ from enum import Enum from typing import Dict, List, Optional, Union -from typing_extensions import Annotated -from pydantic import ( - BaseModel, - BeforeValidator, - ConfigDict, - Field, - RootModel, - field_serializer, -) +from pydantic import (BaseModel, BeforeValidator, ConfigDict, Field, RootModel, + field_serializer) +from typing_extensions import Annotated class PayloadType(str, Enum): diff --git a/harp/reader.py b/harp/reader.py index 91726a0..8f828f3 100644 --- a/harp/reader.py +++ b/harp/reader.py @@ -1,20 +1,23 @@ import os -import requests -from deprecated import deprecated +from collections import UserDict +from dataclasses import dataclass +from datetime import datetime +from functools import partial from math import log2 from os import PathLike from pathlib import Path -from datetime import datetime -from functools import partial -from dataclasses import dataclass +from typing import (Any, BinaryIO, Callable, Iterable, Mapping, Optional, + Protocol, Union) + +import requests +from deprecated import deprecated from numpy import dtype from pandas import DataFrame, Series -from typing import Any, BinaryIO, Callable, Iterable, Mapping, Optional, Protocol, Union -from collections import UserDict from pandas._typing import Axes + from harp import __version__ -from harp.model import BitMask, GroupMask, Model, PayloadMember, Register from harp.io import MessageType, read +from harp.model import BitMask, GroupMask, Model, PayloadMember, Register from harp.schema import read_schema @@ -31,8 +34,7 @@ def __call__( file: Optional[Union[str, bytes, PathLike[Any], BinaryIO]] = None, epoch: Optional[datetime] = None, keep_type: bool = False, - ) -> DataFrame: - ... + ) -> DataFrame: ... class RegisterReader: @@ -310,9 +312,12 @@ def from_dataset( base_path=path, include_common_registers=include_common_registers, epoch=epoch, - keep_type=keep_type) + keep_type=keep_type, + ) else: - raise ValueError("The dataset must be a directory containing a device.yml file.") + raise ValueError( + "The dataset must be a directory containing a device.yml file." + ) def _compose_parser( @@ -462,7 +467,10 @@ def parser(df: DataFrame): reader = partial(reader, columns=[name]) return RegisterReader(register, reader) -@deprecated("This function is deprecated. Use DeviceReader.from_file, DeviceReader.from_url, DeviceReader.from_str, and DeviceReader.from_model instead.") + +@deprecated( + "This function is deprecated. Use DeviceReader.from_file, DeviceReader.from_url, DeviceReader.from_str, and DeviceReader.from_model instead." +) def create_reader( device: Union[str, PathLike, Model], include_common_registers: bool = True, diff --git a/harp/schema.py b/harp/schema.py index 1bb1b0a..0787cc6 100644 --- a/harp/schema.py +++ b/harp/schema.py @@ -1,8 +1,10 @@ +from importlib import resources from os import PathLike from typing import TextIO, Union -from harp.model import Model, Registers + from pydantic_yaml import parse_yaml_raw_as -from importlib import resources + +from harp.model import Model, Registers def _read_common_registers() -> Registers: From 9c8bb7db7cdd86903d4d1b7063a1fe4cf9cdbebd Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Thu, 9 May 2024 08:35:57 -0700 Subject: [PATCH 05/42] Add new constructor methods --- harp/reader.py | 112 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/harp/reader.py b/harp/reader.py index b1b4961..8077705 100644 --- a/harp/reader.py +++ b/harp/reader.py @@ -15,6 +15,7 @@ from harp.io import MessageType, read from harp.model import BitMask, GroupMask, Model, PayloadMember, Register from harp.schema import read_schema +import requests @dataclass @@ -74,6 +75,117 @@ def __dir__(self) -> Iterable[str]: def __getattr__(self, __name: str) -> RegisterReader: return self.registers[__name] + @staticmethod + def from_file( + filepath: PathLike, + base_path: Optional[PathLike] = None, + include_common_registers: bool = True, + epoch: Optional[datetime] = None, + keep_type: bool = False) -> "DeviceReader": + + 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_parser( + device, name, _ReaderParams(base_path, epoch, keep_type) + ) + for name in device.registers.keys() + } + return DeviceReader(device, reg_readers) + + @staticmethod + def from_url( + url: str, + base_path: Optional[PathLike] = None, + include_common_registers: bool = True, + epoch: Optional[datetime] = None, + keep_type: bool = False, + timeout: int = 5) -> "DeviceReader": + + response = requests.get(url, timeout=timeout) + text = response.text + + device = read_schema(text, include_common_registers) + if base_path is None: + base_path = Path(device.device).absolute().resolve() + else: + base_path = Path(base_path).absolute().resolve() + + reg_readers = { + name: _create_register_parser( + device, name, _ReaderParams(base_path, epoch, keep_type) + ) + for name in device.registers.keys() + } + return DeviceReader(device, reg_readers) + + @staticmethod + def from_str( + schema: str, + base_path: Optional[PathLike] = None, + include_common_registers: bool = True, + epoch: Optional[datetime] = None, + keep_type: bool = False) -> "DeviceReader": + + device = read_schema(schema, include_common_registers) + if base_path is None: + base_path = Path(device.device).absolute().resolve() + else: + base_path = Path(base_path).absolute().resolve() + + reg_readers = { + name: _create_register_parser( + device, name, _ReaderParams(base_path, epoch, keep_type) + ) + for name in device.registers.keys() + } + return DeviceReader(device, reg_readers) + + @staticmethod + def from_model( + model: Model, + base_path: Optional[PathLike] = None, + epoch: Optional[datetime] = None, + keep_type: bool = False) -> "DeviceReader": + + if base_path is None: + base_path = Path(model.device).absolute().resolve() + else: + base_path = Path(base_path).absolute().resolve() + + reg_readers = { + name: _create_register_parser( + model, name, _ReaderParams(base_path, epoch, keep_type) + ) + for name in model.registers.keys() + } + return DeviceReader(model, reg_readers) + + @staticmethod + def from_dataset( + dataset: PathLike, + include_common_registers: bool = True, + epoch: Optional[datetime] = None, + keep_type: bool = False) -> "DeviceReader": + + path = Path(dataset).absolute().resolve() + is_dir = os.path.isdir(path) + if is_dir: + filepath = path / "device.yml" + return DeviceReader.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], From a8909915515eaca0eb62f6d6d3642ef467c3349c Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Thu, 9 May 2024 08:44:56 -0700 Subject: [PATCH 06/42] Deprecate function --- harp/reader.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/harp/reader.py b/harp/reader.py index 8077705..6f33aa1 100644 --- a/harp/reader.py +++ b/harp/reader.py @@ -15,7 +15,6 @@ from harp.io import MessageType, read from harp.model import BitMask, GroupMask, Model, PayloadMember, Register from harp.schema import read_schema -import requests @dataclass @@ -334,7 +333,7 @@ def parser(df: DataFrame): reader = partial(reader, columns=[name]) return RegisterReader(register, reader) - +@deprecated("This function is deprecated. Use DeviceReader.from_file, DeviceReader.from_url, DeviceReader.from_str, and DeviceReader.from_model instead.") def create_reader( device: Union[str, PathLike, Model], include_common_registers: bool = True, From 1b3d4385c1ee354d897793ce5969fc956feb3f84 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Thu, 9 May 2024 08:55:49 -0700 Subject: [PATCH 07/42] Document methods --- harp/reader.py | 173 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 149 insertions(+), 24 deletions(-) diff --git a/harp/reader.py b/harp/reader.py index 6f33aa1..8981d8a 100644 --- a/harp/reader.py +++ b/harp/reader.py @@ -76,11 +76,37 @@ def __getattr__(self, __name: str) -> RegisterReader: @staticmethod def from_file( - filepath: PathLike, - base_path: Optional[PathLike] = None, - include_common_registers: bool = True, - epoch: Optional[datetime] = None, - keep_type: bool = False) -> "DeviceReader": + filepath: PathLike, + base_path: Optional[PathLike] = 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: @@ -99,12 +125,39 @@ def from_file( @staticmethod def from_url( - url: str, - base_path: Optional[PathLike] = None, - include_common_registers: bool = True, - epoch: Optional[datetime] = None, - keep_type: bool = False, - timeout: int = 5) -> "DeviceReader": + url: str, + base_path: Optional[PathLike] = 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) text = response.text @@ -125,11 +178,37 @@ def from_url( @staticmethod def from_str( - schema: str, - base_path: Optional[PathLike] = None, - include_common_registers: bool = True, - epoch: Optional[datetime] = None, - keep_type: bool = False) -> "DeviceReader": + schema: str, + base_path: Optional[PathLike] = 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(schema, include_common_registers) if base_path is None: @@ -147,10 +226,32 @@ def from_str( @staticmethod def from_model( - model: Model, - base_path: Optional[PathLike] = None, - epoch: Optional[datetime] = None, - keep_type: bool = False) -> "DeviceReader": + model: Model, + base_path: Optional[PathLike] = 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() @@ -167,10 +268,34 @@ def from_model( @staticmethod def from_dataset( - dataset: PathLike, - include_common_registers: bool = True, - epoch: Optional[datetime] = None, - keep_type: bool = False) -> "DeviceReader": + dataset: PathLike, + 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) From a76c6f0ff7bd4b824f3ace9bd3f7785dfb667637 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Thu, 9 May 2024 08:55:57 -0700 Subject: [PATCH 08/42] Linting --- harp/model.py | 11 +++-------- harp/reader.py | 12 +++++++++--- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/harp/model.py b/harp/model.py index b6f5031..5884b0e 100644 --- a/harp/model.py +++ b/harp/model.py @@ -7,14 +7,9 @@ from enum import Enum from typing import Annotated, Dict, List, Optional, Union -from pydantic import ( - BaseModel, - BeforeValidator, - ConfigDict, - Field, - RootModel, - field_serializer, -) +from pydantic import (BaseModel, BeforeValidator, ConfigDict, Field, RootModel, + field_serializer) +from typing_extensions import Annotated class PayloadType(str, Enum): diff --git a/harp/reader.py b/harp/reader.py index 8981d8a..bbeb2ac 100644 --- a/harp/reader.py +++ b/harp/reader.py @@ -306,9 +306,12 @@ def from_dataset( base_path=path, include_common_registers=include_common_registers, epoch=epoch, - keep_type=keep_type) + keep_type=keep_type, + ) else: - raise ValueError("The dataset must be a directory containing a device.yml file.") + raise ValueError( + "The dataset must be a directory containing a device.yml file." + ) def _compose_parser( @@ -458,7 +461,10 @@ def parser(df: DataFrame): reader = partial(reader, columns=[name]) return RegisterReader(register, reader) -@deprecated("This function is deprecated. Use DeviceReader.from_file, DeviceReader.from_url, DeviceReader.from_str, and DeviceReader.from_model instead.") + +@deprecated( + "This function is deprecated. Use DeviceReader.from_file, DeviceReader.from_url, DeviceReader.from_str, and DeviceReader.from_model instead." +) def create_reader( device: Union[str, PathLike, Model], include_common_registers: bool = True, From 828cd3bac0a452ab4f98652776289ade6b2809be Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:10:36 +0000 Subject: [PATCH 09/42] Fix rebasing --- harp/model.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/harp/model.py b/harp/model.py index 1010fd1..09f30c2 100644 --- a/harp/model.py +++ b/harp/model.py @@ -7,13 +7,7 @@ from enum import Enum from typing import Annotated, Dict, List, Optional, Union -from pydantic import (BaseModel, BeforeValidator, ConfigDict, Field, RootModel, - field_serializer) -from typing import Dict, List, Optional, Union - -from pydantic import (BaseModel, BeforeValidator, ConfigDict, Field, RootModel, - field_serializer) -from typing_extensions import Annotated +from pydantic import BaseModel, BeforeValidator, ConfigDict, Field, RootModel, field_serializer class PayloadType(str, Enum): From 13a68626983a075ba130ffd7f74835f9afd1f349 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:10:51 +0000 Subject: [PATCH 10/42] Add deprecated decorator --- harp/__init__.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/harp/__init__.py b/harp/__init__.py index d3581a5..e7e7877 100644 --- a/harp/__init__.py +++ b/harp/__init__.py @@ -1,5 +1,26 @@ from harp.io import REFERENCE_EPOCH, MessageType, read from harp.reader import create_reader from harp.schema import read_schema +import warnings +import functools + __all__ = ["REFERENCE_EPOCH", "MessageType", "read", "create_reader", "read_schema"] + + +def deprecated(message): + # This decorator is only available from the stdlib warnings module in Python 3.13 + # Making it available here for compatibility with older versions + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + warnings.warn( + f"Call to deprecated function {func.__name__}: {message}", + category=DeprecationWarning, + stacklevel=1, + ) + return func(*args, **kwargs) + + return wrapper + + return decorator From 3cefa8e2b785698645f273f8df23e918d56461b4 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:11:32 +0000 Subject: [PATCH 11/42] Fix rebasing --- harp/reader.py | 243 +------------------------------------------------ 1 file changed, 1 insertion(+), 242 deletions(-) diff --git a/harp/reader.py b/harp/reader.py index 46ccd83..1a7c773 100644 --- a/harp/reader.py +++ b/harp/reader.py @@ -313,248 +313,7 @@ def from_dataset( keep_type=keep_type, ) else: - raise ValueError( - "The dataset must be a directory containing a device.yml file." - ) - - @staticmethod - def from_file( - filepath: PathLike, - base_path: Optional[PathLike] = 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_parser( - device, name, _ReaderParams(base_path, epoch, keep_type) - ) - for name in device.registers.keys() - } - return DeviceReader(device, reg_readers) - - @staticmethod - def from_url( - url: str, - base_path: Optional[PathLike] = 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) - text = response.text - - device = read_schema(text, include_common_registers) - if base_path is None: - base_path = Path(device.device).absolute().resolve() - else: - base_path = Path(base_path).absolute().resolve() - - reg_readers = { - name: _create_register_parser( - device, name, _ReaderParams(base_path, epoch, keep_type) - ) - for name in device.registers.keys() - } - return DeviceReader(device, reg_readers) - - @staticmethod - def from_str( - schema: str, - base_path: Optional[PathLike] = 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(schema, include_common_registers) - if base_path is None: - base_path = Path(device.device).absolute().resolve() - else: - base_path = Path(base_path).absolute().resolve() - - reg_readers = { - name: _create_register_parser( - device, name, _ReaderParams(base_path, epoch, keep_type) - ) - for name in device.registers.keys() - } - return DeviceReader(device, reg_readers) - - @staticmethod - def from_model( - model: Model, - base_path: Optional[PathLike] = 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() - - reg_readers = { - name: _create_register_parser( - model, name, _ReaderParams(base_path, epoch, keep_type) - ) - for name in model.registers.keys() - } - return DeviceReader(model, reg_readers) - - @staticmethod - def from_dataset( - dataset: PathLike, - 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 DeviceReader.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." - ) + raise ValueError("The dataset must be a directory containing a device.yml file.") def _compose_parser( From 89870d9c953ec525409fa9b3213043b7384f0462 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:12:17 +0000 Subject: [PATCH 12/42] Favor library's deprecated decorator --- harp/reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/harp/reader.py b/harp/reader.py index 1a7c773..aae311b 100644 --- a/harp/reader.py +++ b/harp/reader.py @@ -10,7 +10,7 @@ Protocol, Union) import requests -from deprecated import deprecated +from harp import deprecated from numpy import dtype from pandas import DataFrame, Series from pandas._typing import Axes From 83397d146641ec8871a72bdfb549959f3f63251b Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:12:32 +0000 Subject: [PATCH 13/42] Linting --- harp/reader.py | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/harp/reader.py b/harp/reader.py index aae311b..ebc0d28 100644 --- a/harp/reader.py +++ b/harp/reader.py @@ -6,8 +6,7 @@ from math import log2 from os import PathLike from pathlib import Path -from typing import (Any, BinaryIO, Callable, Iterable, Mapping, Optional, - Protocol, Union) +from typing import Any, BinaryIO, Callable, Iterable, Mapping, Optional, Protocol, Union import requests from harp import deprecated @@ -15,7 +14,6 @@ from pandas import DataFrame, Series from pandas._typing import Axes -from harp import __version__ from harp.io import MessageType, read from harp.model import BitMask, GroupMask, Model, PayloadMember, Register from harp.schema import read_schema @@ -120,9 +118,7 @@ def from_file( base_path = Path(base_path).absolute().resolve() / device.device reg_readers = { - name: _create_register_parser( - device, name, _ReaderParams(base_path, epoch, keep_type) - ) + name: _create_register_parser(device, name, _ReaderParams(base_path, epoch, keep_type)) for name in device.registers.keys() } return DeviceReader(device, reg_readers) @@ -173,9 +169,7 @@ def from_url( base_path = Path(base_path).absolute().resolve() reg_readers = { - name: _create_register_parser( - device, name, _ReaderParams(base_path, epoch, keep_type) - ) + name: _create_register_parser(device, name, _ReaderParams(base_path, epoch, keep_type)) for name in device.registers.keys() } return DeviceReader(device, reg_readers) @@ -221,9 +215,7 @@ def from_str( base_path = Path(base_path).absolute().resolve() reg_readers = { - name: _create_register_parser( - device, name, _ReaderParams(base_path, epoch, keep_type) - ) + name: _create_register_parser(device, name, _ReaderParams(base_path, epoch, keep_type)) for name in device.registers.keys() } return DeviceReader(device, reg_readers) @@ -263,9 +255,7 @@ def from_model( base_path = Path(base_path).absolute().resolve() reg_readers = { - name: _create_register_parser( - model, name, _ReaderParams(base_path, epoch, keep_type) - ) + name: _create_register_parser(model, name, _ReaderParams(base_path, epoch, keep_type)) for name in model.registers.keys() } return DeviceReader(model, reg_readers) From 48dadeb7dfbaa930739a611302c5d73a96976a66 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:16:46 +0000 Subject: [PATCH 14/42] Add requests as dependency --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d038094..8833988 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,8 @@ license = {text = "MIT License"} dependencies = [ "pydantic-yaml", - "pandas" + "pandas", + "requests" ] classifiers = [ From 4c39bd89b2349b075b0b20607b1ff9fb6b6ea696 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:22:58 +0000 Subject: [PATCH 15/42] Move decorator to separate private helper module to prevent circular dependencies --- harp/__init__.py | 20 -------------------- harp/_helpers.py | 20 ++++++++++++++++++++ harp/reader.py | 2 +- 3 files changed, 21 insertions(+), 21 deletions(-) create mode 100644 harp/_helpers.py diff --git a/harp/__init__.py b/harp/__init__.py index e7e7877..962e5fe 100644 --- a/harp/__init__.py +++ b/harp/__init__.py @@ -1,26 +1,6 @@ from harp.io import REFERENCE_EPOCH, MessageType, read from harp.reader import create_reader from harp.schema import read_schema -import warnings -import functools __all__ = ["REFERENCE_EPOCH", "MessageType", "read", "create_reader", "read_schema"] - - -def deprecated(message): - # This decorator is only available from the stdlib warnings module in Python 3.13 - # Making it available here for compatibility with older versions - def decorator(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - warnings.warn( - f"Call to deprecated function {func.__name__}: {message}", - category=DeprecationWarning, - stacklevel=1, - ) - return func(*args, **kwargs) - - return wrapper - - return decorator diff --git a/harp/_helpers.py b/harp/_helpers.py new file mode 100644 index 0000000..0dbec61 --- /dev/null +++ b/harp/_helpers.py @@ -0,0 +1,20 @@ +import warnings +import functools + + +def deprecated(message): + # This decorator is only available from the stdlib warnings module in Python 3.13 + # Making it available here for compatibility with older versions + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + warnings.warn( + f"Call to deprecated function {func.__name__}: {message}", + category=DeprecationWarning, + stacklevel=1, + ) + return func(*args, **kwargs) + + return wrapper + + return decorator diff --git a/harp/reader.py b/harp/reader.py index ebc0d28..e3fec6c 100644 --- a/harp/reader.py +++ b/harp/reader.py @@ -9,7 +9,7 @@ from typing import Any, BinaryIO, Callable, Iterable, Mapping, Optional, Protocol, Union import requests -from harp import deprecated +from harp._helpers import deprecated from numpy import dtype from pandas import DataFrame, Series from pandas._typing import Axes From 132cb738883a2510c102f0084c7805a6c3213c86 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:24:46 +0000 Subject: [PATCH 16/42] Ignore warnings emitted by deprecated decorator --- tests/test_reader.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_reader.py b/tests/test_reader.py index ba038f8..842ce63 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -20,6 +20,7 @@ @mark.parametrize("schemaFile", testdata) +@mark.filterwarnings("ignore:Call to deprecated") def test_create_reader(schemaFile: DeviceSchemaParam): reader = create_reader(schemaFile.path, epoch=REFERENCE_EPOCH) schemaFile.assert_schema(reader.device) From 30ecc6bd23ea0a7e80969fbe3559222669d72705 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sat, 22 Mar 2025 09:22:07 -0700 Subject: [PATCH 17/42] Favor stdlib decorator --- harp/_helpers.py | 20 -------------------- harp/reader.py | 2 +- 2 files changed, 1 insertion(+), 21 deletions(-) delete mode 100644 harp/_helpers.py diff --git a/harp/_helpers.py b/harp/_helpers.py deleted file mode 100644 index 0dbec61..0000000 --- a/harp/_helpers.py +++ /dev/null @@ -1,20 +0,0 @@ -import warnings -import functools - - -def deprecated(message): - # This decorator is only available from the stdlib warnings module in Python 3.13 - # Making it available here for compatibility with older versions - def decorator(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - warnings.warn( - f"Call to deprecated function {func.__name__}: {message}", - category=DeprecationWarning, - stacklevel=1, - ) - return func(*args, **kwargs) - - return wrapper - - return decorator diff --git a/harp/reader.py b/harp/reader.py index c64de07..f7459a9 100644 --- a/harp/reader.py +++ b/harp/reader.py @@ -9,7 +9,7 @@ from typing import Callable, Iterable, Mapping, Optional, Protocol, Union import requests -from harp._helpers import deprecated +from typing_extensions import deprecated from numpy import dtype from pandas import DataFrame, Series from pandas._typing import Axes From c09c57312d7425d45690e3711cfeea11890ba97e Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sat, 22 Mar 2025 09:23:02 -0700 Subject: [PATCH 18/42] Refactor to #40 --- harp/reader.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/harp/reader.py b/harp/reader.py index f7459a9..05c0a8e 100644 --- a/harp/reader.py +++ b/harp/reader.py @@ -77,8 +77,9 @@ def __dir__(self) -> Iterable[str]: def __getattr__(self, __name: str) -> RegisterReader: return self.registers[__name] - @staticmethod + @classmethod def from_file( + cls, filepath: PathLike, base_path: Optional[PathLike] = None, include_common_registers: bool = True, @@ -119,13 +120,14 @@ def from_file( base_path = Path(base_path).absolute().resolve() / device.device reg_readers = { - name: _create_register_parser(device, name, _ReaderParams(base_path, epoch, keep_type)) + name: _create_register_handler(device, name, _ReaderParams(base_path, epoch, keep_type)) for name in device.registers.keys() } - return DeviceReader(device, reg_readers) + return cls(device, reg_readers) - @staticmethod + @classmethod def from_url( + cls, url: str, base_path: Optional[PathLike] = None, include_common_registers: bool = True, @@ -170,13 +172,14 @@ def from_url( base_path = Path(base_path).absolute().resolve() reg_readers = { - name: _create_register_parser(device, name, _ReaderParams(base_path, epoch, keep_type)) + name: _create_register_handler(device, name, _ReaderParams(base_path, epoch, keep_type)) for name in device.registers.keys() } - return DeviceReader(device, reg_readers) + return cls(device, reg_readers) - @staticmethod + @classmethod def from_str( + cls, schema: str, base_path: Optional[PathLike] = None, include_common_registers: bool = True, @@ -216,13 +219,14 @@ def from_str( base_path = Path(base_path).absolute().resolve() reg_readers = { - name: _create_register_parser(device, name, _ReaderParams(base_path, epoch, keep_type)) + name: _create_register_handler(device, name, _ReaderParams(base_path, epoch, keep_type)) for name in device.registers.keys() } - return DeviceReader(device, reg_readers) + return cls(device, reg_readers) - @staticmethod + @classmethod def from_model( + cls, model: Model, base_path: Optional[PathLike] = None, epoch: Optional[datetime] = None, @@ -256,13 +260,14 @@ def from_model( base_path = Path(base_path).absolute().resolve() reg_readers = { - name: _create_register_parser(model, name, _ReaderParams(base_path, epoch, keep_type)) + name: _create_register_handler(model, name, _ReaderParams(base_path, epoch, keep_type)) for name in model.registers.keys() } - return DeviceReader(model, reg_readers) + return cls(model, reg_readers) - @staticmethod + @classmethod def from_dataset( + cls, dataset: PathLike, include_common_registers: bool = True, epoch: Optional[datetime] = None, @@ -296,7 +301,7 @@ def from_dataset( is_dir = os.path.isdir(path) if is_dir: filepath = path / "device.yml" - return DeviceReader.from_file( + return cls.from_file( filepath=filepath, base_path=path, include_common_registers=include_common_registers, From cc258c1e5e50642b9bd6d2712dd1906a4d499de8 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sat, 22 Mar 2025 09:23:24 -0700 Subject: [PATCH 19/42] Linting --- harp/reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/harp/reader.py b/harp/reader.py index 05c0a8e..5434780 100644 --- a/harp/reader.py +++ b/harp/reader.py @@ -9,10 +9,10 @@ from typing import Callable, Iterable, Mapping, Optional, Protocol, Union import requests -from typing_extensions import deprecated from numpy import dtype from pandas import DataFrame, Series from pandas._typing import Axes +from typing_extensions import deprecated from harp.io import MessageType, read from harp.model import BitMask, GroupMask, Model, PayloadMember, Register From 02059bffa5c464d27883d473da7c93f81c088a5b Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Tue, 1 Apr 2025 11:12:59 -0700 Subject: [PATCH 20/42] Make `data` input to `_compose_parser` optional This allows `g` aka `_create_register_reader` to take care of the path inference logic --- harp/reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/harp/reader.py b/harp/reader.py index 590164c..de130ad 100644 --- a/harp/reader.py +++ b/harp/reader.py @@ -82,7 +82,7 @@ def _compose_parser( 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, From 6a882c275ae9320535da6ad8cf47b276a0a81726 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Tue, 1 Apr 2025 14:24:30 -0700 Subject: [PATCH 21/42] Add unittest for registers with payload spec --- tests/data/device.yml | 16 ++++++++++++++++ tests/test_reader.py | 5 +++++ 2 files changed, 21 insertions(+) diff --git a/tests/data/device.yml b/tests/data/device.yml index 583f290..636a6cf 100644 --- a/tests/data/device.yml +++ b/tests/data/device.yml @@ -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. diff --git a/tests/test_reader.py b/tests/test_reader.py index 41c2776..0dc838f 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -16,6 +16,11 @@ expected_whoAmI=0, expected_registers=["AnalogData"], ), + DeviceSchemaParam( + path="data/device.yml", + expected_whoAmI=0, + expected_registers=["AnalogDataPayloadSpec"], + ), ] From 3ee94ae1f8491b4a135dd91d601777ca35b3bfa7 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Tue, 1 Apr 2025 14:33:43 -0700 Subject: [PATCH 22/42] Suppress `pyright` error on internal method import --- harp/io.py | 2 +- harp/reader.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/harp/io.py b/harp/io.py index 5b9091b..2b750c0 100644 --- a/harp/io.py +++ b/harp/io.py @@ -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 diff --git a/harp/reader.py b/harp/reader.py index de130ad..bcc76f6 100644 --- a/harp/reader.py +++ b/harp/reader.py @@ -10,7 +10,7 @@ from numpy import dtype from pandas import DataFrame, Series -from pandas._typing import Axes +from pandas._typing import Axes # pyright: ignore[reportPrivateImportUsage] from harp.io import MessageType, read from harp.model import BitMask, GroupMask, Model, PayloadMember, Register From 204b2256938e925636b1df9567c5460524d7b2ae Mon Sep 17 00:00:00 2001 From: glopesdev Date: Wed, 2 Apr 2025 17:12:03 +0100 Subject: [PATCH 23/42] Replace license classifier with SPDX expression --- pyproject.toml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f8264b0..c8fcaac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ 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", @@ -20,8 +20,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] From edd9415138b82cf0eb3f357863cc04e542c61ece Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Thu, 9 May 2024 08:35:57 -0700 Subject: [PATCH 24/42] Add new constructor methods --- harp/reader.py | 112 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/harp/reader.py b/harp/reader.py index bcc76f6..d1f00d3 100644 --- a/harp/reader.py +++ b/harp/reader.py @@ -16,6 +16,7 @@ from harp.model import BitMask, GroupMask, Model, PayloadMember, Register from harp.schema import read_schema from harp.typing import _BufferLike, _FileLike +import requests @dataclass @@ -75,6 +76,117 @@ def __dir__(self) -> Iterable[str]: def __getattr__(self, __name: str) -> RegisterReader: return self.registers[__name] + @staticmethod + def from_file( + filepath: PathLike, + base_path: Optional[PathLike] = None, + include_common_registers: bool = True, + epoch: Optional[datetime] = None, + keep_type: bool = False) -> "DeviceReader": + + 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_parser( + device, name, _ReaderParams(base_path, epoch, keep_type) + ) + for name in device.registers.keys() + } + return DeviceReader(device, reg_readers) + + @staticmethod + def from_url( + url: str, + base_path: Optional[PathLike] = None, + include_common_registers: bool = True, + epoch: Optional[datetime] = None, + keep_type: bool = False, + timeout: int = 5) -> "DeviceReader": + + response = requests.get(url, timeout=timeout) + text = response.text + + device = read_schema(text, include_common_registers) + if base_path is None: + base_path = Path(device.device).absolute().resolve() + else: + base_path = Path(base_path).absolute().resolve() + + reg_readers = { + name: _create_register_parser( + device, name, _ReaderParams(base_path, epoch, keep_type) + ) + for name in device.registers.keys() + } + return DeviceReader(device, reg_readers) + + @staticmethod + def from_str( + schema: str, + base_path: Optional[PathLike] = None, + include_common_registers: bool = True, + epoch: Optional[datetime] = None, + keep_type: bool = False) -> "DeviceReader": + + device = read_schema(schema, include_common_registers) + if base_path is None: + base_path = Path(device.device).absolute().resolve() + else: + base_path = Path(base_path).absolute().resolve() + + reg_readers = { + name: _create_register_parser( + device, name, _ReaderParams(base_path, epoch, keep_type) + ) + for name in device.registers.keys() + } + return DeviceReader(device, reg_readers) + + @staticmethod + def from_model( + model: Model, + base_path: Optional[PathLike] = None, + epoch: Optional[datetime] = None, + keep_type: bool = False) -> "DeviceReader": + + if base_path is None: + base_path = Path(model.device).absolute().resolve() + else: + base_path = Path(base_path).absolute().resolve() + + reg_readers = { + name: _create_register_parser( + model, name, _ReaderParams(base_path, epoch, keep_type) + ) + for name in model.registers.keys() + } + return DeviceReader(model, reg_readers) + + @staticmethod + def from_dataset( + dataset: PathLike, + include_common_registers: bool = True, + epoch: Optional[datetime] = None, + keep_type: bool = False) -> "DeviceReader": + + path = Path(dataset).absolute().resolve() + is_dir = os.path.isdir(path) + if is_dir: + filepath = path / "device.yml" + return DeviceReader.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], From 7662754a30cca373b724892234373783e5a49ab4 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Thu, 9 May 2024 08:44:56 -0700 Subject: [PATCH 25/42] Deprecate function --- harp/reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/harp/reader.py b/harp/reader.py index d1f00d3..36b538e 100644 --- a/harp/reader.py +++ b/harp/reader.py @@ -340,7 +340,7 @@ def payload_parser(df: DataFrame): reader = partial(reader, columns=columns) return RegisterReader(register, reader) - +@deprecated("This function is deprecated. Use DeviceReader.from_file, DeviceReader.from_url, DeviceReader.from_str, and DeviceReader.from_model instead.") def create_reader( device: Union[str, PathLike, Model], include_common_registers: bool = True, From ceaa56d1a080000ea5fb990d79d14d30dccf4555 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Thu, 9 May 2024 08:55:49 -0700 Subject: [PATCH 26/42] Document methods --- harp/reader.py | 173 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 149 insertions(+), 24 deletions(-) diff --git a/harp/reader.py b/harp/reader.py index 36b538e..72fdb17 100644 --- a/harp/reader.py +++ b/harp/reader.py @@ -78,11 +78,37 @@ def __getattr__(self, __name: str) -> RegisterReader: @staticmethod def from_file( - filepath: PathLike, - base_path: Optional[PathLike] = None, - include_common_registers: bool = True, - epoch: Optional[datetime] = None, - keep_type: bool = False) -> "DeviceReader": + filepath: PathLike, + base_path: Optional[PathLike] = 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: @@ -101,12 +127,39 @@ def from_file( @staticmethod def from_url( - url: str, - base_path: Optional[PathLike] = None, - include_common_registers: bool = True, - epoch: Optional[datetime] = None, - keep_type: bool = False, - timeout: int = 5) -> "DeviceReader": + url: str, + base_path: Optional[PathLike] = 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) text = response.text @@ -127,11 +180,37 @@ def from_url( @staticmethod def from_str( - schema: str, - base_path: Optional[PathLike] = None, - include_common_registers: bool = True, - epoch: Optional[datetime] = None, - keep_type: bool = False) -> "DeviceReader": + schema: str, + base_path: Optional[PathLike] = 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(schema, include_common_registers) if base_path is None: @@ -149,10 +228,32 @@ def from_str( @staticmethod def from_model( - model: Model, - base_path: Optional[PathLike] = None, - epoch: Optional[datetime] = None, - keep_type: bool = False) -> "DeviceReader": + model: Model, + base_path: Optional[PathLike] = 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() @@ -169,10 +270,34 @@ def from_model( @staticmethod def from_dataset( - dataset: PathLike, - include_common_registers: bool = True, - epoch: Optional[datetime] = None, - keep_type: bool = False) -> "DeviceReader": + dataset: PathLike, + 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) From 7c8e5b40a4cd391bace6682e270a64ad7bd687a8 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Thu, 9 May 2024 08:55:57 -0700 Subject: [PATCH 27/42] Linting --- harp/reader.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/harp/reader.py b/harp/reader.py index 72fdb17..887c7f9 100644 --- a/harp/reader.py +++ b/harp/reader.py @@ -308,9 +308,12 @@ def from_dataset( base_path=path, include_common_registers=include_common_registers, epoch=epoch, - keep_type=keep_type) + keep_type=keep_type, + ) else: - raise ValueError("The dataset must be a directory containing a device.yml file.") + raise ValueError( + "The dataset must be a directory containing a device.yml file." + ) def _compose_parser( @@ -465,7 +468,10 @@ def payload_parser(df: DataFrame): reader = partial(reader, columns=columns) return RegisterReader(register, reader) -@deprecated("This function is deprecated. Use DeviceReader.from_file, DeviceReader.from_url, DeviceReader.from_str, and DeviceReader.from_model instead.") + +@deprecated( + "This function is deprecated. Use DeviceReader.from_file, DeviceReader.from_url, DeviceReader.from_str, and DeviceReader.from_model instead." +) def create_reader( device: Union[str, PathLike, Model], include_common_registers: bool = True, From b5160a82a1fc6b92467c72e1a39497973beeec84 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:10:51 +0000 Subject: [PATCH 28/42] Add deprecated decorator --- harp/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/harp/__init__.py b/harp/__init__.py index ba6211c..269d9d2 100644 --- a/harp/__init__.py +++ b/harp/__init__.py @@ -1,5 +1,8 @@ from harp.io import REFERENCE_EPOCH, MessageType, read, to_buffer, to_file from harp.reader import create_reader from harp.schema import read_schema +import warnings +import functools + __all__ = ["REFERENCE_EPOCH", "MessageType", "read", "to_buffer", "to_file", "create_reader", "read_schema"] From 58f3a0b4f70e5f387c7c1e7fb86471f65397f671 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:12:32 +0000 Subject: [PATCH 29/42] Linting --- harp/reader.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/harp/reader.py b/harp/reader.py index 887c7f9..43b5904 100644 --- a/harp/reader.py +++ b/harp/reader.py @@ -118,9 +118,7 @@ def from_file( base_path = Path(base_path).absolute().resolve() / device.device reg_readers = { - name: _create_register_parser( - device, name, _ReaderParams(base_path, epoch, keep_type) - ) + name: _create_register_parser(device, name, _ReaderParams(base_path, epoch, keep_type)) for name in device.registers.keys() } return DeviceReader(device, reg_readers) @@ -171,9 +169,7 @@ def from_url( base_path = Path(base_path).absolute().resolve() reg_readers = { - name: _create_register_parser( - device, name, _ReaderParams(base_path, epoch, keep_type) - ) + name: _create_register_parser(device, name, _ReaderParams(base_path, epoch, keep_type)) for name in device.registers.keys() } return DeviceReader(device, reg_readers) @@ -219,9 +215,7 @@ def from_str( base_path = Path(base_path).absolute().resolve() reg_readers = { - name: _create_register_parser( - device, name, _ReaderParams(base_path, epoch, keep_type) - ) + name: _create_register_parser(device, name, _ReaderParams(base_path, epoch, keep_type)) for name in device.registers.keys() } return DeviceReader(device, reg_readers) @@ -261,9 +255,7 @@ def from_model( base_path = Path(base_path).absolute().resolve() reg_readers = { - name: _create_register_parser( - model, name, _ReaderParams(base_path, epoch, keep_type) - ) + name: _create_register_parser(model, name, _ReaderParams(base_path, epoch, keep_type)) for name in model.registers.keys() } return DeviceReader(model, reg_readers) From a9cea29de3e0239c3925b76b4d35241db92d0af0 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:16:46 +0000 Subject: [PATCH 30/42] Add requests as dependency --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c8fcaac..102ece7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,8 @@ license = "MIT" dependencies = [ "pydantic-yaml", - "pandas" + "pandas", + "requests" ] classifiers = [ From b4abcaa3504c7af490947571fc04fb225c30fc70 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:22:58 +0000 Subject: [PATCH 31/42] Move decorator to separate private helper module to prevent circular dependencies --- harp/__init__.py | 2 -- harp/_helpers.py | 20 ++++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 harp/_helpers.py diff --git a/harp/__init__.py b/harp/__init__.py index 269d9d2..9aa6d6d 100644 --- a/harp/__init__.py +++ b/harp/__init__.py @@ -1,8 +1,6 @@ from harp.io import REFERENCE_EPOCH, MessageType, read, to_buffer, to_file from harp.reader import create_reader from harp.schema import read_schema -import warnings -import functools __all__ = ["REFERENCE_EPOCH", "MessageType", "read", "to_buffer", "to_file", "create_reader", "read_schema"] diff --git a/harp/_helpers.py b/harp/_helpers.py new file mode 100644 index 0000000..0dbec61 --- /dev/null +++ b/harp/_helpers.py @@ -0,0 +1,20 @@ +import warnings +import functools + + +def deprecated(message): + # This decorator is only available from the stdlib warnings module in Python 3.13 + # Making it available here for compatibility with older versions + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + warnings.warn( + f"Call to deprecated function {func.__name__}: {message}", + category=DeprecationWarning, + stacklevel=1, + ) + return func(*args, **kwargs) + + return wrapper + + return decorator From e00c78456674064ae488996927d3f7a68a5e9e6c Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:24:46 +0000 Subject: [PATCH 32/42] Ignore warnings emitted by deprecated decorator --- tests/test_reader.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_reader.py b/tests/test_reader.py index 0dc838f..6b1dbe6 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -25,6 +25,7 @@ @mark.parametrize("schemaFile", testdata) +@mark.filterwarnings("ignore:Call to deprecated") def test_create_reader(schemaFile: DeviceSchemaParam): reader = create_reader(schemaFile.path, epoch=REFERENCE_EPOCH) schemaFile.assert_schema(reader.device) From 5a2b4090586c97987be1390ba9f6de88a61a5b38 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sat, 22 Mar 2025 09:22:07 -0700 Subject: [PATCH 33/42] Favor stdlib decorator --- harp/_helpers.py | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 harp/_helpers.py diff --git a/harp/_helpers.py b/harp/_helpers.py deleted file mode 100644 index 0dbec61..0000000 --- a/harp/_helpers.py +++ /dev/null @@ -1,20 +0,0 @@ -import warnings -import functools - - -def deprecated(message): - # This decorator is only available from the stdlib warnings module in Python 3.13 - # Making it available here for compatibility with older versions - def decorator(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - warnings.warn( - f"Call to deprecated function {func.__name__}: {message}", - category=DeprecationWarning, - stacklevel=1, - ) - return func(*args, **kwargs) - - return wrapper - - return decorator From 70cea112c5a7cdeb168bb9b3d97a0ea53e45e7c6 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sat, 22 Mar 2025 09:23:02 -0700 Subject: [PATCH 34/42] Refactor to #40 --- harp/reader.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/harp/reader.py b/harp/reader.py index 43b5904..0f7d66d 100644 --- a/harp/reader.py +++ b/harp/reader.py @@ -76,8 +76,9 @@ def __dir__(self) -> Iterable[str]: def __getattr__(self, __name: str) -> RegisterReader: return self.registers[__name] - @staticmethod + @classmethod def from_file( + cls, filepath: PathLike, base_path: Optional[PathLike] = None, include_common_registers: bool = True, @@ -118,13 +119,14 @@ def from_file( base_path = Path(base_path).absolute().resolve() / device.device reg_readers = { - name: _create_register_parser(device, name, _ReaderParams(base_path, epoch, keep_type)) + name: _create_register_handler(device, name, _ReaderParams(base_path, epoch, keep_type)) for name in device.registers.keys() } - return DeviceReader(device, reg_readers) + return cls(device, reg_readers) - @staticmethod + @classmethod def from_url( + cls, url: str, base_path: Optional[PathLike] = None, include_common_registers: bool = True, @@ -169,13 +171,14 @@ def from_url( base_path = Path(base_path).absolute().resolve() reg_readers = { - name: _create_register_parser(device, name, _ReaderParams(base_path, epoch, keep_type)) + name: _create_register_handler(device, name, _ReaderParams(base_path, epoch, keep_type)) for name in device.registers.keys() } - return DeviceReader(device, reg_readers) + return cls(device, reg_readers) - @staticmethod + @classmethod def from_str( + cls, schema: str, base_path: Optional[PathLike] = None, include_common_registers: bool = True, @@ -215,13 +218,14 @@ def from_str( base_path = Path(base_path).absolute().resolve() reg_readers = { - name: _create_register_parser(device, name, _ReaderParams(base_path, epoch, keep_type)) + name: _create_register_handler(device, name, _ReaderParams(base_path, epoch, keep_type)) for name in device.registers.keys() } - return DeviceReader(device, reg_readers) + return cls(device, reg_readers) - @staticmethod + @classmethod def from_model( + cls, model: Model, base_path: Optional[PathLike] = None, epoch: Optional[datetime] = None, @@ -255,13 +259,14 @@ def from_model( base_path = Path(base_path).absolute().resolve() reg_readers = { - name: _create_register_parser(model, name, _ReaderParams(base_path, epoch, keep_type)) + name: _create_register_handler(model, name, _ReaderParams(base_path, epoch, keep_type)) for name in model.registers.keys() } - return DeviceReader(model, reg_readers) + return cls(model, reg_readers) - @staticmethod + @classmethod def from_dataset( + cls, dataset: PathLike, include_common_registers: bool = True, epoch: Optional[datetime] = None, @@ -295,7 +300,7 @@ def from_dataset( is_dir = os.path.isdir(path) if is_dir: filepath = path / "device.yml" - return DeviceReader.from_file( + return cls.from_file( filepath=filepath, base_path=path, include_common_registers=include_common_registers, From d063806110b3e99185b9ec206cb86667b53cd762 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Thu, 5 Jun 2025 00:40:29 -0700 Subject: [PATCH 35/42] Linting --- harp/__init__.py | 1 - harp/reader.py | 5 +---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/harp/__init__.py b/harp/__init__.py index 9aa6d6d..ba6211c 100644 --- a/harp/__init__.py +++ b/harp/__init__.py @@ -2,5 +2,4 @@ from harp.reader import create_reader from harp.schema import read_schema - __all__ = ["REFERENCE_EPOCH", "MessageType", "read", "to_buffer", "to_file", "create_reader", "read_schema"] diff --git a/harp/reader.py b/harp/reader.py index 3f5f3ff..80f2d20 100644 --- a/harp/reader.py +++ b/harp/reader.py @@ -17,7 +17,6 @@ from harp.model import BitMask, GroupMask, Model, PayloadMember, Register from harp.schema import read_schema from harp.typing import _BufferLike, _FileLike -import requests @dataclass @@ -309,9 +308,7 @@ def from_dataset( keep_type=keep_type, ) else: - raise ValueError( - "The dataset must be a directory containing a device.yml file." - ) + raise ValueError("The dataset must be a directory containing a device.yml file.") @classmethod def from_file( From 724bca1c1d62c740fa5ebb39c0b736a6a422881b Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Thu, 5 Jun 2025 00:52:50 -0700 Subject: [PATCH 36/42] Import deprecated from typing_extensions --- harp/reader.py | 235 +------------------------------------------------ 1 file changed, 1 insertion(+), 234 deletions(-) diff --git a/harp/reader.py b/harp/reader.py index 80f2d20..9109580 100644 --- a/harp/reader.py +++ b/harp/reader.py @@ -12,6 +12,7 @@ from numpy import dtype from pandas import DataFrame, Series 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 @@ -310,240 +311,6 @@ def from_dataset( else: raise ValueError("The dataset must be a directory containing a device.yml file.") - @classmethod - def from_file( - cls, - filepath: PathLike, - base_path: Optional[PathLike] = 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[PathLike] = 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) - text = response.text - - device = read_schema(text, include_common_registers) - if base_path is None: - base_path = Path(device.device).absolute().resolve() - else: - base_path = Path(base_path).absolute().resolve() - - 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_str( - cls, - schema: str, - base_path: Optional[PathLike] = 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(schema, include_common_registers) - if base_path is None: - base_path = Path(device.device).absolute().resolve() - else: - base_path = Path(base_path).absolute().resolve() - - 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[PathLike] = 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() - - 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: PathLike, - 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], From 7a4a81a672416809fbf2e01db90b8cd452ad3758 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Wed, 18 Jun 2025 02:32:35 -0700 Subject: [PATCH 37/42] Ensure device_name is appended to base_path --- harp/reader.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/harp/reader.py b/harp/reader.py index 9109580..edee7ae 100644 --- a/harp/reader.py +++ b/harp/reader.py @@ -3,6 +3,7 @@ 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 @@ -164,18 +165,13 @@ def from_url( response = requests.get(url, timeout=timeout) text = response.text - - device = read_schema(text, include_common_registers) - if base_path is None: - base_path = Path(device.device).absolute().resolve() - else: - base_path = Path(base_path).absolute().resolve() - - 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) + 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( @@ -211,12 +207,12 @@ def from_str( can be accessed using dot notation using the name of the register as the key. """ - - device = read_schema(schema, include_common_registers) + 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)) @@ -258,6 +254,7 @@ def from_model( 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)) From 1e075ee09f1c7dba620737e29998a2ef937a191c Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Wed, 18 Jun 2025 02:32:42 -0700 Subject: [PATCH 38/42] Add unittest --- tests/test_reader.py | 55 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/tests/test_reader.py b/tests/test_reader.py index 6b1dbe6..23676b0 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -1,8 +1,10 @@ +from unittest.mock import Mock, patch + import pandas as pd from pytest import mark from harp.io import REFERENCE_EPOCH, MessageType -from harp.reader import create_reader +from harp.reader import DeviceReader, Model, create_reader, read_schema from tests.params import DeviceSchemaParam testdata = [ @@ -24,10 +26,7 @@ ] -@mark.parametrize("schemaFile", testdata) -@mark.filterwarnings("ignore:Call to deprecated") -def test_create_reader(schemaFile: DeviceSchemaParam): - reader = create_reader(schemaFile.path, epoch=REFERENCE_EPOCH) +def helper_test_reader(reader: DeviceReader, schemaFile: DeviceSchemaParam) -> None: schemaFile.assert_schema(reader.device) whoAmI = reader.WhoAmI.read() @@ -42,3 +41,49 @@ def test_create_reader(schemaFile: DeviceSchemaParam): for register_name in schemaFile.expected_registers: data = reader.registers[register_name].read() assert isinstance(data.index, pd.DatetimeIndex) + + +@mark.parametrize("schemaFile", testdata) +@mark.filterwarnings("ignore:Call to deprecated") +def test_create_reader(schemaFile: DeviceSchemaParam): + reader = create_reader(schemaFile.path, epoch=REFERENCE_EPOCH) + helper_test_reader(reader, schemaFile) + + +@mark.parametrize("schemaFile", testdata) +def test_create_reader_from_file(schemaFile: DeviceSchemaParam): + reader = DeviceReader.from_file("./tests/data/device.yml", epoch=REFERENCE_EPOCH) + helper_test_reader(reader, schemaFile) + + +@mark.parametrize("schemaFile", testdata) +def test_create_reader_from_dataset(schemaFile: DeviceSchemaParam): + reader = DeviceReader.from_dataset("./tests/data", epoch=REFERENCE_EPOCH) + helper_test_reader(reader, schemaFile) + + +@mark.parametrize("schemaFile", testdata) +def test_create_reader_from_str(schemaFile: DeviceSchemaParam): + with open("./tests/data/device.yml", "r", encoding="utf-8") as f: + reader = DeviceReader.from_str(f.read(), base_path="./tests/data/", epoch=REFERENCE_EPOCH) + helper_test_reader(reader, schemaFile) + + +@mark.parametrize("schemaFile", testdata) +def test_create_reader_from_model(schemaFile: DeviceSchemaParam): + model = read_schema("./tests/data/device.yml", include_common_registers=True) + reader = DeviceReader.from_model(model=model, base_path="./tests/data/", epoch=REFERENCE_EPOCH) + helper_test_reader(reader, schemaFile) + + +@mark.parametrize("schemaFile", testdata) +@patch("requests.get") +def test_create_reader_from_url(mock_url_get, schemaFile: DeviceSchemaParam): + mock_response = Mock() + mock_response.status_code = 200 + mock_response.text = open("./tests/data/device.yml", "r", encoding="utf-8").read() + + mock_url_get.return_value = mock_response + + reader = DeviceReader.from_url("mocked_url", base_path="./tests/data/", epoch=REFERENCE_EPOCH) + helper_test_reader(reader, schemaFile) From c4db1373b68f6a8d918dbf2ea7aeb0c4de8657ce Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Wed, 18 Jun 2025 02:35:00 -0700 Subject: [PATCH 39/42] Add string type hinting to paths --- harp/reader.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/harp/reader.py b/harp/reader.py index edee7ae..47d282f 100644 --- a/harp/reader.py +++ b/harp/reader.py @@ -81,8 +81,8 @@ def __getattr__(self, __name: str) -> RegisterReader: @classmethod def from_file( cls, - filepath: PathLike, - base_path: Optional[PathLike] = None, + filepath: PathLike | str, + base_path: Optional[PathLike | str] = None, include_common_registers: bool = True, epoch: Optional[datetime] = None, keep_type: bool = False, @@ -130,7 +130,7 @@ def from_file( def from_url( cls, url: str, - base_path: Optional[PathLike] = None, + base_path: Optional[PathLike | str] = None, include_common_registers: bool = True, epoch: Optional[datetime] = None, keep_type: bool = False, @@ -177,7 +177,7 @@ def from_url( def from_str( cls, schema: str, - base_path: Optional[PathLike] = None, + base_path: Optional[PathLike | str] = None, include_common_registers: bool = True, epoch: Optional[datetime] = None, keep_type: bool = False, @@ -224,7 +224,7 @@ def from_str( def from_model( cls, model: Model, - base_path: Optional[PathLike] = None, + base_path: Optional[PathLike | str] = None, epoch: Optional[datetime] = None, keep_type: bool = False, ) -> "DeviceReader": @@ -265,7 +265,7 @@ def from_model( @classmethod def from_dataset( cls, - dataset: PathLike, + dataset: PathLike | str, include_common_registers: bool = True, epoch: Optional[datetime] = None, keep_type: bool = False, From f6843d2dc748b450f1dc68255a0df97dbf9a1b80 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Wed, 18 Jun 2025 02:38:38 -0700 Subject: [PATCH 40/42] Refactor unions to legacy syntax --- harp/reader.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/harp/reader.py b/harp/reader.py index 47d282f..0499248 100644 --- a/harp/reader.py +++ b/harp/reader.py @@ -81,8 +81,8 @@ def __getattr__(self, __name: str) -> RegisterReader: @classmethod def from_file( cls, - filepath: PathLike | str, - base_path: Optional[PathLike | str] = None, + filepath: Union[PathLike, str], + base_path: Optional[Union[PathLike, str]] = None, include_common_registers: bool = True, epoch: Optional[datetime] = None, keep_type: bool = False, @@ -130,7 +130,7 @@ def from_file( def from_url( cls, url: str, - base_path: Optional[PathLike | str] = None, + base_path: Optional[Union[PathLike, str]] = None, include_common_registers: bool = True, epoch: Optional[datetime] = None, keep_type: bool = False, @@ -177,7 +177,7 @@ def from_url( def from_str( cls, schema: str, - base_path: Optional[PathLike | str] = None, + base_path: Optional[Union[PathLike, str]] = None, include_common_registers: bool = True, epoch: Optional[datetime] = None, keep_type: bool = False, @@ -224,7 +224,7 @@ def from_str( def from_model( cls, model: Model, - base_path: Optional[PathLike | str] = None, + base_path: Optional[Union[PathLike, str]] = None, epoch: Optional[datetime] = None, keep_type: bool = False, ) -> "DeviceReader": @@ -265,7 +265,7 @@ def from_model( @classmethod def from_dataset( cls, - dataset: PathLike | str, + dataset: Union[PathLike, str], include_common_registers: bool = True, epoch: Optional[datetime] = None, keep_type: bool = False, From b1c522eb3f9dc5613e506b2488e02f7925141133 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Wed, 18 Jun 2025 02:47:57 -0700 Subject: [PATCH 41/42] Ensure request errors are raised from reader --- harp/reader.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/harp/reader.py b/harp/reader.py index 0499248..97986ec 100644 --- a/harp/reader.py +++ b/harp/reader.py @@ -164,7 +164,9 @@ def from_url( """ response = requests.get(url, timeout=timeout) + response.raise_for_status() text = response.text + return cls.from_str( text, base_path=base_path, From 3dfa3e611e7258c395196c029bfcd916a50aee94 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Wed, 18 Jun 2025 02:49:28 -0700 Subject: [PATCH 42/42] Shorten deprecation warning --- harp/reader.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/harp/reader.py b/harp/reader.py index 97986ec..27a91f4 100644 --- a/harp/reader.py +++ b/harp/reader.py @@ -464,9 +464,7 @@ def payload_parser(df: DataFrame): return RegisterReader(register, reader) -@deprecated( - "This function is deprecated. Use DeviceReader.from_file, DeviceReader.from_url, DeviceReader.from_str, and DeviceReader.from_model instead." -) +@deprecated("This function is deprecated. Use DeviceReader.from_* methods instead.") def create_reader( device: Union[str, PathLike, Model], include_common_registers: bool = True,