From be0c7a063212f2b0c0efdfda79a0d89264c38865 Mon Sep 17 00:00:00 2001 From: Joshua Vasquez Date: Wed, 13 Apr 2022 17:02:02 -0700 Subject: [PATCH 001/267] can now pip instal in editable mode --- README.md | 16 +++++++++++++--- pyproject.toml | 4 ++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4b18b2d..11c2463 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,20 @@ - - # pyharp Harp implementation of the Harp protocol. -## Edit the code +## Install with Pip +From this directory, create a *setup.py* file with +```` +poetry build +```` +and then install in editable mode with +```` +pip install -e . +``` + +Note that for the above to work, a fairly recent version of pip (>= 21.3) is required. + +## Install with Poetry Each Python user has is own very dear IDE for editing. Here, we are leaving instructions on how to edit this code using pyCharm, Anaconda and Poetry. diff --git a/pyproject.toml b/pyproject.toml index 62037dc..ca8d302 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,8 +16,8 @@ mypy = "^0.782" black = "^19.10b0" [build-system] -requires = ["poetry>=0.12"] -build-backend = "poetry.masonry.api" +requires = ["poetry-core>=1.0.8"] +build-backend = "poetry.core.masonry.api" [tool.poetry.scripts] pyharp = "pyharp.main:main" From ef9936e2fd46e8c06ae907e0614ae424442fd784 Mon Sep 17 00:00:00 2001 From: Joshua Vasquez Date: Wed, 13 Apr 2022 17:02:48 -0700 Subject: [PATCH 002/267] updating readme --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index 11c2463..7b5a181 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,7 @@ Harp implementation of the Harp protocol. ## Install with Pip -From this directory, create a *setup.py* file with -```` -poetry build -```` -and then install in editable mode with +From this directory, install in editable mode with ```` pip install -e . ``` From 69e11f0b1f6850eb998a40b6a59f3a5f677dcdb1 Mon Sep 17 00:00:00 2001 From: Joshua Vasquez Date: Wed, 13 Apr 2022 17:03:44 -0700 Subject: [PATCH 003/267] fixing readme bug --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7b5a181..cea031f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Harp implementation of the Harp protocol. From this directory, install in editable mode with ```` pip install -e . -``` +```` Note that for the above to work, a fairly recent version of pip (>= 21.3) is required. From 4fff7bab10d0d4284ab2b5ba656f72bf0f0cdfdd Mon Sep 17 00:00:00 2001 From: Joshua Vasquez Date: Wed, 13 Apr 2022 17:32:34 -0700 Subject: [PATCH 004/267] adding udev rules --- 10-ftdi.rules | 3 +++ README.md | 14 ++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 10-ftdi.rules diff --git a/10-ftdi.rules b/10-ftdi.rules new file mode 100644 index 0000000..b960fec --- /dev/null +++ b/10-ftdi.rules @@ -0,0 +1,3 @@ +# UDEV rules for an ftdi RS232 Serial (Uart) IC +SUBSYSTEMS=="usb", ENV{.LOCAL_ifNum}="$attr{bInterfaceNumber}" +SUBSYSTEMS=="usb", KERNEL=="ttyUSB*", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", MODE="0666", SYMLINK+="ftdi-uart_%E{.LOCAL_ifNum}" diff --git a/README.md b/README.md index cea031f..1a38b41 100644 --- a/README.md +++ b/README.md @@ -75,3 +75,17 @@ Device info: * Firmware version: 1.0 * Device user name: IBL_rig_0 ``` + +## For Linux Only + +### UDEV rules +either copy `10-ftdi.rules` into your /etc/udev/rules.d folder or symlink it with +```` +sudo ln -s /full/path/to/pyharp/10-ftdi.rules /etc/udev/rules.d/10-ftdi.rules + +```` + +Reload udev rules with: +```` +sudo udevadm control --reload-rules +```` From d974ee403ccf3d311318a78a6a813cfe9fce1fa2 Mon Sep 17 00:00:00 2001 From: Joshua Vasquez Date: Wed, 13 Apr 2022 18:20:27 -0700 Subject: [PATCH 005/267] device_names is now a default dict; examples now also work on linux --- 10-ftdi.rules | 2 +- examples/check_device_id.py | 11 +++- examples/get_info.py | 10 +++- examples/write_and_read_from_registers.py | 10 +++- pyharp/device.py | 2 +- pyharp/device_names.py | 67 +++++++++-------------- 6 files changed, 53 insertions(+), 49 deletions(-) mode change 100644 => 100755 examples/check_device_id.py mode change 100644 => 100755 examples/get_info.py mode change 100644 => 100755 examples/write_and_read_from_registers.py diff --git a/10-ftdi.rules b/10-ftdi.rules index b960fec..91f9aff 100644 --- a/10-ftdi.rules +++ b/10-ftdi.rules @@ -1,3 +1,3 @@ # UDEV rules for an ftdi RS232 Serial (Uart) IC SUBSYSTEMS=="usb", ENV{.LOCAL_ifNum}="$attr{bInterfaceNumber}" -SUBSYSTEMS=="usb", KERNEL=="ttyUSB*", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", MODE="0666", SYMLINK+="ftdi-uart_%E{.LOCAL_ifNum}" +SUBSYSTEMS=="usb", KERNEL=="ttyUSB*", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", MODE="0666", SYMLINK+="harp_device_%E{.LOCAL_ifNum}" diff --git a/examples/check_device_id.py b/examples/check_device_id.py old mode 100644 new mode 100755 index b118b0e..237e20e --- a/examples/check_device_id.py +++ b/examples/check_device_id.py @@ -1,7 +1,10 @@ +#!/usr/bin/env python3 from pyharp.device import Device from pyharp.messages import HarpMessage from pyharp.messages import MessageType +from pyharp.device_names import device_names from struct import * +import os # ON THIS EXAMPLE @@ -11,7 +14,11 @@ # Open the device -device = Device("COM95") # Open serial connection +# Open serial connection +if os.name == "posix": # check for Linux. + device = Device("/dev/ttyUSB0") +else: # assume Windows. + device = Device("COM95") # Get some of the device's parameters device_id = device.WHO_AM_I # Get device's ID @@ -19,7 +26,7 @@ device_user_name = device.DEVICE_NAME # Get device's user name # Check if we are dealing with the correct device -if device_id == 2080: +if device_id in device_names: print("Correct device was found!") print(f"Device's ID: {device_id}") print(f"Device's name: {device_id_description}") diff --git a/examples/get_info.py b/examples/get_info.py old mode 100644 new mode 100755 index fca840c..56be774 --- a/examples/get_info.py +++ b/examples/get_info.py @@ -1,7 +1,9 @@ +#!/usr/bin/env python3 from pyharp.device import Device from pyharp.messages import HarpMessage from pyharp.messages import MessageType from struct import * +import os # ON THIS EXAMPLE @@ -11,7 +13,11 @@ # Open the device and print the info on screen -device = Device("COM95", "ibl.bin") # Open serial connection and save communication to a file +# Open serial connection and save communication to a file +if os.name == 'posix': # check for Linux. + device = Device("/dev/harp_device_00", "ibl.bin") +else: # assume Windows. + device = Device("COM95", "ibl.bin") device.info() # Display device's info on screen # Get some of the device's parameters @@ -29,4 +35,4 @@ device_assembly = device.ASSEMBLY_VERSION # Get device's assembly version # Close connection -device.disconnect() \ No newline at end of file +device.disconnect() diff --git a/examples/write_and_read_from_registers.py b/examples/write_and_read_from_registers.py old mode 100644 new mode 100755 index ccde127..ceb6d52 --- a/examples/write_and_read_from_registers.py +++ b/examples/write_and_read_from_registers.py @@ -1,7 +1,9 @@ +#!/usr/bin/env python3 from pyharp.device import Device from pyharp.messages import HarpMessage from pyharp.messages import MessageType from struct import * +import os # ON THIS EXAMPLE @@ -12,7 +14,11 @@ # Open the device and print the info on screen -device = Device("COM95", "ibl.bin") # Open serial connection and save communication to a file +# Open serial connection and save communication to a file +if os.name == 'posix': # check for Linux. + device = Device("/dev/harp_device_00", "ibl.bin") +else: # assume Windows. + device = Device("COM95", "ibl.bin") # Read current analog sensor's higher threshold (ANA_SENSOR_TH0_HIGH) at address 42 analog_threshold_h = device.send(HarpMessage.ReadU16(42).frame).payload_as_int() @@ -34,4 +40,4 @@ print(f"Analog sensor's values: {analog_sensor}") # Close connection -device.disconnect() \ No newline at end of file +device.disconnect() diff --git a/pyharp/device.py b/pyharp/device.py index 5e0e64d..25ecf31 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -4,7 +4,7 @@ from pyharp.messages import HarpMessage, ReplyHarpMessage from pyharp.messages import CommonRegisters -from pyharp import device_names +from pyharp.device_names import device_names class Device: diff --git a/pyharp/device_names.py b/pyharp/device_names.py index e5d8cbd..d7b7557 100644 --- a/pyharp/device_names.py +++ b/pyharp/device_names.py @@ -1,41 +1,26 @@ -def get(value: int) -> str: - if value == 1024: - return "Poke" - elif value == 1040: - return "MultiPwm" - elif value == 1056: - return "Wear" - elif value == 1072: - return "VoltsDrive" - elif value == 1088: - return "LedController" - elif value == 1104: - return "Synchronizer" - elif value == 1121: - return "SimpleAnalogGenerator" - elif value == 1136: - return "Archimedes" - elif value == 1152: - return "ClockSynchronizer" - elif value == 1168: - return "Camera" - elif value == 1184: - return "PyControl" - elif value == 1200: - return "FlyPad" - elif value == 1216: - return "Behavior" - elif value == 1232: - return "LoadCells" - elif value == 1248: - return "AudioSwitch" - elif value == 1264: - return "Rgb" - elif value == 1200: - return "FlyPad" - elif value == 2064: - return "FP3002" - elif value == 2080: - return "IblBehavior" - else: - return "NotSpecified" +from collections import defaultdict + + +current_device_names = \ + {1024: 'Poke', + 1040: 'MultiPwm', + 1056: 'Wear', + 1072: 'VoltsDrive', + 1088: 'LedController', + 1104: 'Synchronizer', + 1121: 'SimpleAnalogGenerator', + 1136: 'Archimedes', + 1152: 'ClockSynchronizer', + 1168: 'Camera', + 1184: 'PyControl', + 1200: 'FlyPad', + 1216: 'Behavior', + 1232: 'LoadCells', + 1248: 'AudioSwitch', + 1264: 'Rgb', + 1200: 'FlyPad', + 2064: 'FP3002', + 2080: 'IblBehavior'} +device_names = defaultdict(lambda: 'NotSpecified') +device_names.update(current_device_names) + From fbdacbd4d12c19feb653815b46ca318d3932e1a0 Mon Sep 17 00:00:00 2001 From: Joshua Vasquez Date: Fri, 15 Apr 2022 14:52:15 -0700 Subject: [PATCH 006/267] fixing pdf link; tweaking serial port to use ftdi rule --- examples/check_device_id.py | 2 +- pyharp/device.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/check_device_id.py b/examples/check_device_id.py index 237e20e..9568921 100755 --- a/examples/check_device_id.py +++ b/examples/check_device_id.py @@ -16,7 +16,7 @@ # Open the device # Open serial connection if os.name == "posix": # check for Linux. - device = Device("/dev/ttyUSB0") + device = Device("/dev/harp_device_00") else: # assume Windows. device = Device("COM95") diff --git a/pyharp/device.py b/pyharp/device.py index 25ecf31..b145f9c 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -9,7 +9,7 @@ class Device: """ - https://github.com/harp-tech/protocol/blob/master/Device%201.0%201.3%2020190207.pdf + https://github.com/harp-tech/protocol/blob/master/Device%201.1%201.0%2020220402.pdf """ _ser: serial.Serial From 33d8cc896d7462658fce5cf74f422d61476912d0 Mon Sep 17 00:00:00 2001 From: Joshua Vasquez Date: Fri, 15 Apr 2022 14:56:29 -0700 Subject: [PATCH 007/267] Update README.md --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1a38b41..b506d9b 100644 --- a/README.md +++ b/README.md @@ -76,16 +76,16 @@ Device info: * Device user name: IBL_rig_0 ``` -## For Linux Only +## for Linux -### UDEV rules -either copy `10-ftdi.rules` into your /etc/udev/rules.d folder or symlink it with -```` -sudo ln -s /full/path/to/pyharp/10-ftdi.rules /etc/udev/rules.d/10-ftdi.rules +### Install UDEV Rules +Install by either copying `10-harp.rules` over to your `/etc/udev/rules.d` folder or by symlinking it with: +```` +sudo ln -s /absolute/path/to/vibratome-controller/10-harp.rules /etc/udev/rules.d/10-harp.rules ```` -Reload udev rules with: +Then reload udev rules with ```` sudo udevadm control --reload-rules ```` From 0191b237bafb2fe32bdff1e58777b3eb9059ab59 Mon Sep 17 00:00:00 2001 From: Joshua Vasquez Date: Mon, 18 Apr 2022 13:41:57 -0700 Subject: [PATCH 008/267] PEP 563 return type hints --- pyharp/messages.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/pyharp/messages.py b/pyharp/messages.py index 383ae2e..866e3f9 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -1,3 +1,4 @@ +from __future__ import annotations # for type hints (PEP 563) # from abc import ABC, abstractmethod from typing import Union, Tuple, Optional @@ -81,39 +82,39 @@ def message_type(self) -> int: return self._frame[0] @staticmethod - def ReadU8(address: int) -> "ReadU8HarpMessage": + def ReadU8(address: int) -> ReadU8HarpMessage: return ReadU8HarpMessage(address) @staticmethod - def ReadS8(address: int) -> "ReadS8HarpMessage": + def ReadS8(address: int) -> ReadS8HarpMessage: return ReadS8HarpMessage(address) @staticmethod - def ReadS16(address: int) -> "ReadS16HarpMessage": + def ReadS16(address: int) -> ReadS16HarpMessage: return ReadS16HarpMessage(address) @staticmethod - def ReadU16(address: int) -> "ReadU16HarpMessage": + def ReadU16(address: int) -> ReadU16HarpMessage: return ReadU16HarpMessage(address) @staticmethod - def WriteU8(address: int, value: int) -> "WriteU8HarpMessage": + def WriteU8(address: int, value: int) -> WriteU8HarpMessage: return WriteU8HarpMessage(address, value) @staticmethod - def WriteS8(address: int, value: int) -> "WriteS8HarpMessage": + def WriteS8(address: int, value: int) -> WriteS8HarpMessage: return WriteS8HarpMessage(address, value) @staticmethod - def WriteS16(address: int, value: int) -> "WriteS16HarpMessage": + def WriteS16(address: int, value: int) -> WriteS16HarpMessage: return WriteS16HarpMessage(address, value) @staticmethod - def WriteU16(address: int, value: int) -> "WriteU16HarpMessage": + def WriteU16(address: int, value: int) -> WriteU16HarpMessage: return WriteU16HarpMessage(address, value) @staticmethod - def parse(frame: bytearray) -> "ReplyHarpMessage": + def parse(frame: bytearray) -> ReplyHarpMessage: return ReplyHarpMessage(frame) From 6a73a9932c0969e2915ac993205ff2a0d312ab2f Mon Sep 17 00:00:00 2001 From: Joshua Vasquez Date: Mon, 18 Apr 2022 18:41:27 -0700 Subject: [PATCH 009/267] grand messages.py refactor --- 10-ftdi.rules | 3 - examples/write_and_read_from_registers.py | 13 +- pyharp/messages.py | 190 ++++++++-------------- 3 files changed, 78 insertions(+), 128 deletions(-) delete mode 100644 10-ftdi.rules diff --git a/10-ftdi.rules b/10-ftdi.rules deleted file mode 100644 index 91f9aff..0000000 --- a/10-ftdi.rules +++ /dev/null @@ -1,3 +0,0 @@ -# UDEV rules for an ftdi RS232 Serial (Uart) IC -SUBSYSTEMS=="usb", ENV{.LOCAL_ifNum}="$attr{bInterfaceNumber}" -SUBSYSTEMS=="usb", KERNEL=="ttyUSB*", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", MODE="0666", SYMLINK+="harp_device_%E{.LOCAL_ifNum}" diff --git a/examples/write_and_read_from_registers.py b/examples/write_and_read_from_registers.py index ceb6d52..6f6f029 100755 --- a/examples/write_and_read_from_registers.py +++ b/examples/write_and_read_from_registers.py @@ -21,8 +21,17 @@ device = Device("COM95", "ibl.bin") # Read current analog sensor's higher threshold (ANA_SENSOR_TH0_HIGH) at address 42 -analog_threshold_h = device.send(HarpMessage.ReadU16(42).frame).payload_as_int() -print(f"Analog sensor's higher threshold: {analog_threshold_h}") +#analog_threshold_h = device.send(HarpMessage.ReadU16(42).frame).payload_as_int() +#print(f"Analog sensor's higher threshold: {analog_threshold_h}") + + +data_stream = device.send(HarpMessage.ReadU8(33).frame) # returns a ReplyHarpMessage +#data_stream = device.send(HarpMessage.ReadS16(33).frame).payload_as_int_array() +print(f"Data Stream payload type: {data_stream.payload_type}") +print(f"Data Stream message type: {data_stream.message_type}") +print(f"Data Stream timestamp: {data_stream.timestamp}") +print(f"Data Stream num bytes: {data_stream.length}") +print(f"Data Stream: {data_stream.payload}") # Increase current analog sensor's higher threshold by one unit device.send(HarpMessage.WriteU16(42, analog_threshold_h+1).frame) diff --git a/pyharp/messages.py b/pyharp/messages.py index 866e3f9..3b09d2d 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -1,9 +1,10 @@ from __future__ import annotations # for type hints (PEP 563) +from enum import Enum # from abc import ABC, abstractmethod from typing import Union, Tuple, Optional -class MessageType: +class MessageType(Enum): READ: int = 1 WRITE: int = 2 EVENT: int = 3 @@ -11,7 +12,7 @@ class MessageType: WRITE_ERROR: int = 10 -class PayloadType: +class PayloadType(Enum): isUnsigned: int = 0x00 isSigned: int = 0x80 isFloat: int = 0x40 @@ -37,8 +38,17 @@ class PayloadType: TimestampedS64 = hasTimestamp | S64 TimestampedFloat = hasTimestamp | Float - ALL_UNSIGNED = [U8, U16, U32, TimestampedU8, TimestampedU16] - ALL_SIGNED = [S8, S16, S32, TimestampedS8, TimestampedS16] + +ALL_UNSIGNED = [PayloadType.U8, + PayloadType.U16, + PayloadType.U32, + PayloadType.TimestampedU8, + PayloadType.TimestampedU16] +ALL_SIGNED = [PayloadType.S8, + PayloadType.S16, + PayloadType.S32, + PayloadType.TimestampedS8, + PayloadType.TimestampedS16] class CommonRegisters: @@ -61,6 +71,10 @@ class CommonRegisters: class HarpMessage: + """ + https://github.com/harp-tech/protocol/blob/master/Binary%20Protocol%201.0%201.1%2020180223.pdf + """ + DEFAULT_PORT: int = 255 _frame: bytearray @@ -79,7 +93,27 @@ def frame(self) -> bytearray: @property def message_type(self) -> int: - return self._frame[0] + return MessageType(self._frame[0]) + + @property + def length(self) -> int: + return self._frame[1] + + @property + def address(self) -> int: + return self._frame[2] + + @property + def port(self) -> int: + return self._frame[3] + + @property + def payload_type(self) -> int: + return PayloadType(self._frame[4]) + + @property + def checksum(self) -> int: + return self._frame[-1] @staticmethod def ReadU8(address: int) -> ReadU8HarpMessage: @@ -118,6 +152,7 @@ def parse(frame: bytearray) -> ReplyHarpMessage: return ReplyHarpMessage(frame) +# A Response Message from a harp device. class ReplyHarpMessage(HarpMessage): PAYLOAD_START_ADDRESS: int PAYLOAD_LAST_ADDRESS: int @@ -141,16 +176,10 @@ def __init__( self._frame = frame - self._message_type = frame[0] - self._length = frame[1] - self._address = frame[2] - self._port = frame[3] - self._payload_type = frame[4] - # TOOO: add timestamp here - self._payload = frame[ - 11:-1 - ] # retrieve all content from 11 (where payload starts) until the checksum (not inclusive) - self._checksum = frame[-1] # last index is the checksum + self._timestamp = int.from_bytes(frame[5:9], byteorder="little", signed=False) + \ + int.from_bytes(frame[9:11], byteorder="little", signed=False)*32e-6 + # retrieve all content from 11 (where payload starts) until the checksum (not inclusive) + self._payload = frame[11:-1] # print(f"Type: {self.message_type}") # print(f"Length: {self.length}") @@ -161,61 +190,43 @@ def __init__( # print(f"Checksum: {self.checksum}") # print(f"Frame: {self.frame}") - @property - def frame(self) -> bytearray: - return self._frame - - @property - def message_type(self) -> int: - return self._message_type - - @property - def length(self) -> int: - return self._length - - @property - def address(self) -> int: - return self._address - - @property - def port(self) -> int: - return self._port - - @property - def payload_type(self) -> int: - return self._payload_type - @property def payload(self) -> bytes: return self._payload + @property + def timestamp(self) -> float: + return self._timestamp + def payload_as_int(self) -> int: value: int = 0 - if self.payload_type in PayloadType.ALL_UNSIGNED: + if self.payload_type in ALL_UNSIGNED: value = int.from_bytes(self.payload, byteorder="little", signed=False) - elif self.payload_type in PayloadType.ALL_SIGNED: + elif self.payload_type in ALL_SIGNED: value = int.from_bytes(self.payload, byteorder="little", signed=True) return value - def payload_as_int_array(self): - pass # TODO: implement this + def payload_as_int_array(self) -> list: + datatype_bytes = 0x0F & self.payload_type.value # number of bytes per chunk: 1, 2, 4, or 8. + # TODO: is len(self.payload) == self.length? + signed = True if self.payload_type in ALL_UNSIGNED else False + # Break the payload into chunks of datatype size in bytes + byte_chunks = [self.payload[i: i+datatype_bytes] for i in range(0, len(self.payload), datatype_bytes)] + return [int.from_bytes(chunk, byteorder="little", signed=signed) for chunk in byte_chunks] def payload_as_string(self) -> str: return self.payload.decode("utf-8") - @property - def checksum(self) -> int: - return self._checksum - +# A Read Request Message sent to a harp device. class ReadHarpMessage(HarpMessage): - MESSAGE_TYPE: int = MessageType.READ - _length: int - _address: int - _payload_type: int + MESSAGE_TYPE: int = MessageType.READ.value + _length: int # length of this message minus checksum. + _address: int # address to read from # address to read from + _payload_type: int # p _checksum: int - def __init__(self, payload_type: int, address: int): + def __init__(self, payload_type: PayloadType, address: int): self._frame = bytearray() self._frame.append(self.MESSAGE_TYPE) @@ -225,42 +236,9 @@ def __init__(self, payload_type: int, address: int): self._frame.append(address) self._frame.append(self.DEFAULT_PORT) - self._frame.append(payload_type) + self._frame.append(payload_type.value) self._frame.append(self.calculate_checksum()) - # def calculate_checksum(self) -> int: - # return ( - # self.message_type - # + self.length - # + self.address - # + self.port - # + self.payload_type - # ) & 255 - - @property - def message_type(self) -> int: - return self._frame[0] - - @property - def length(self) -> int: - return self._frame[1] - - @property - def address(self) -> int: - return self._frame[2] - - @property - def port(self) -> int: - return self._frame[3] - - @property - def payload_type(self) -> int: - return self._frame[4] - - @property - def checksum(self) -> int: - return self._frame[5] - class ReadU8HarpMessage(ReadHarpMessage): def __init__(self, address: int): @@ -284,7 +262,7 @@ def __init__(self, address: int): class WriteHarpMessage(HarpMessage): BASE_LENGTH: int = 5 - MESSAGE_TYPE: int = MessageType.WRITE + MESSAGE_TYPE: int = MessageType.WRITE.value _length: int _address: int _payload_type: int @@ -292,7 +270,7 @@ class WriteHarpMessage(HarpMessage): _checksum: int def __init__( - self, payload_type: int, payload: bytes, address: int, offset: int = 0 + self, payload_type: PayloadType, payload: bytes, address: int, offset: int = 0 ): """ @@ -309,47 +287,13 @@ def __init__( self._frame.append(address) self._frame.append(HarpMessage.DEFAULT_PORT) - self._frame.append(payload_type) + self._frame.append(payload_type.value) for i in payload: self._frame.append(i) self._frame.append(self.calculate_checksum()) - # def calculate_checksum(self) -> int: - # return ( - # self.message_type - # + self.length - # + self.address - # + self.port - # + self.payload_type - # + self.payload - # ) & 255 - - @property - def message_type(self) -> int: - return self._frame[0] - - @property - def length(self) -> int: - return self._frame[1] - - @property - def address(self) -> int: - return self._frame[2] - - @property - def port(self) -> int: - return self._frame[3] - - @property - def payload_type(self) -> int: - return self._frame[4] - - @property - def checksum(self) -> int: - return self._frame[-1] - class WriteU8HarpMessage(WriteHarpMessage): def __init__(self, address: int, value: int): From 4108f6b76e63111ba2fa97817cfefd3f07b2a95c Mon Sep 17 00:00:00 2001 From: Joshua Vasquez Date: Mon, 18 Apr 2022 18:58:27 -0700 Subject: [PATCH 010/267] more message.py refactor --- examples/write_and_read_from_registers.py | 4 +-- pyharp/messages.py | 44 ++++++----------------- 2 files changed, 13 insertions(+), 35 deletions(-) diff --git a/examples/write_and_read_from_registers.py b/examples/write_and_read_from_registers.py index 6f6f029..ae7ba96 100755 --- a/examples/write_and_read_from_registers.py +++ b/examples/write_and_read_from_registers.py @@ -27,8 +27,8 @@ data_stream = device.send(HarpMessage.ReadU8(33).frame) # returns a ReplyHarpMessage #data_stream = device.send(HarpMessage.ReadS16(33).frame).payload_as_int_array() -print(f"Data Stream payload type: {data_stream.payload_type}") -print(f"Data Stream message type: {data_stream.message_type}") +print(f"Data Stream payload type: {data_stream.payload_type.name}") +print(f"Data Stream message type: {data_stream.message_type.name}") print(f"Data Stream timestamp: {data_stream.timestamp}") print(f"Data Stream num bytes: {data_stream.length}") print(f"Data Stream: {data_stream.payload}") diff --git a/pyharp/messages.py b/pyharp/messages.py index 3b09d2d..434a134 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -67,9 +67,6 @@ class CommonRegisters: DEVICE_NAME = 0x0C -T = Union[int, bytearray] - - class HarpMessage: """ https://github.com/harp-tech/protocol/blob/master/Binary%20Protocol%201.0%201.1%2020180223.pdf @@ -154,38 +151,27 @@ def parse(frame: bytearray) -> ReplyHarpMessage: # A Response Message from a harp device. class ReplyHarpMessage(HarpMessage): - PAYLOAD_START_ADDRESS: int - PAYLOAD_LAST_ADDRESS: int - _message_type: int - _length: int - _address: int - _payload_type: int - _payload: bytes - _checksum: int + def __init__( self, frame: bytearray, ): """ - :param payload_type: - :param payload: - :param address: - :param offset: how many bytes more besides the length corresponding to U8 (for example, for U16 it would be offset=1) + :param frame: the serialized message frame. """ self._frame = frame - self._timestamp = int.from_bytes(frame[5:9], byteorder="little", signed=False) + \ int.from_bytes(frame[9:11], byteorder="little", signed=False)*32e-6 # retrieve all content from 11 (where payload starts) until the checksum (not inclusive) self._payload = frame[11:-1] - # print(f"Type: {self.message_type}") + # print(f"Type: {self.message_type.name}") # print(f"Length: {self.length}") # print(f"Address: {self.address}") # print(f"Port: {self.port}") - # print(f"Payload Type: {self.payload_type}") + # print(f"Payload Type: {self.payload_type.name}") # print(f"Payload: {self.payload}") # print(f"Checksum: {self.checksum}") # print(f"Frame: {self.frame}") @@ -207,7 +193,8 @@ def payload_as_int(self) -> int: return value def payload_as_int_array(self) -> list: - datatype_bytes = 0x0F & self.payload_type.value # number of bytes per chunk: 1, 2, 4, or 8. + # Number of bytes per chunk. Get this from the bit field structure. + datatype_bytes = 0x0F & self.payload_type.value # TODO: is len(self.payload) == self.length? signed = True if self.payload_type in ALL_UNSIGNED else False # Break the payload into chunks of datatype size in bytes @@ -220,20 +207,16 @@ def payload_as_string(self) -> str: # A Read Request Message sent to a harp device. class ReadHarpMessage(HarpMessage): - MESSAGE_TYPE: int = MessageType.READ.value - _length: int # length of this message minus checksum. - _address: int # address to read from # address to read from - _payload_type: int # p - _checksum: int + MESSAGE_TYPE: int = MessageType.READ + def __init__(self, payload_type: PayloadType, address: int): self._frame = bytearray() - self._frame.append(self.MESSAGE_TYPE) + self._frame.append(self.MESSAGE_TYPE.value) length: int = 4 self._frame.append(length) - self._frame.append(address) self._frame.append(self.DEFAULT_PORT) self._frame.append(payload_type.value) @@ -262,12 +245,7 @@ def __init__(self, address: int): class WriteHarpMessage(HarpMessage): BASE_LENGTH: int = 5 - MESSAGE_TYPE: int = MessageType.WRITE.value - _length: int - _address: int - _payload_type: int - _payload: int - _checksum: int + MESSAGE_TYPE: int = MessageType.WRITE def __init__( self, payload_type: PayloadType, payload: bytes, address: int, offset: int = 0 @@ -281,7 +259,7 @@ def __init__( """ self._frame = bytearray() - self._frame.append(self.MESSAGE_TYPE) + self._frame.append(self.MESSAGE_TYPE.value) self._frame.append(self.BASE_LENGTH + offset) From e3457bb148824ea869a09d68b899e67beeafa3cd Mon Sep 17 00:00:00 2001 From: Joshua Vasquez Date: Tue, 19 Apr 2022 16:10:16 -0700 Subject: [PATCH 011/267] adding harp rules --- 10-harp.rules | 3 ++ examples/get_info.py | 2 +- examples/write_and_read_from_registers.py | 32 ++++++------- pyharp/device.py | 56 ++++++++++++++++++++--- 4 files changed, 70 insertions(+), 23 deletions(-) create mode 100644 10-harp.rules diff --git a/10-harp.rules b/10-harp.rules new file mode 100644 index 0000000..c5114ef --- /dev/null +++ b/10-harp.rules @@ -0,0 +1,3 @@ +# UDEV rules for a Harp Device (actually an ftdi RS232 Serial [Uart] IC) +SUBSYSTEMS=="usb", ENV{.LOCAL_ifNum}="$attr{bInterfaceNumber}" +SUBSYSTEMS=="usb", KERNEL=="ttyUSB*", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", MODE="0666", SYMLINK+="harp_device_%E{.LOCAL_ifNum}" diff --git a/examples/get_info.py b/examples/get_info.py index 56be774..44e4f18 100755 --- a/examples/get_info.py +++ b/examples/get_info.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -from pyharp.device import Device +from pyharp.device import Device, DeviceMode from pyharp.messages import HarpMessage from pyharp.messages import MessageType from struct import * diff --git a/examples/write_and_read_from_registers.py b/examples/write_and_read_from_registers.py index ae7ba96..3634353 100755 --- a/examples/write_and_read_from_registers.py +++ b/examples/write_and_read_from_registers.py @@ -31,22 +31,22 @@ print(f"Data Stream message type: {data_stream.message_type.name}") print(f"Data Stream timestamp: {data_stream.timestamp}") print(f"Data Stream num bytes: {data_stream.length}") -print(f"Data Stream: {data_stream.payload}") - -# Increase current analog sensor's higher threshold by one unit -device.send(HarpMessage.WriteU16(42, analog_threshold_h+1).frame) - -# Check if the register was well written -analog_threshold_h = device.send(HarpMessage.ReadU16(42).frame).payload_as_int() -print(f"Analog sensor's higher threshold: {analog_threshold_h}") - -# Read 10 samples of the analog sensor and display the values -# The value is at register STREAM[0], address 33 -analog_sensor = [] -for x in range(10): - value = device.send(HarpMessage.ReadS16(33).frame).payload_as_int() - analog_sensor.append(value & 0xffff) -print(f"Analog sensor's values: {analog_sensor}") +print(f"Data Stream payload: {data_stream.payload}") + +## Increase current analog sensor's higher threshold by one unit +#device.send(HarpMessage.WriteU16(42, analog_threshold_h+1).frame) +# +## Check if the register was well written +#analog_threshold_h = device.send(HarpMessage.ReadU16(42).frame).payload_as_int() +#print(f"Analog sensor's higher threshold: {analog_threshold_h}") +# +## Read 10 samples of the analog sensor and display the values +## The value is at register STREAM[0], address 33 +#analog_sensor = [] +#for x in range(10): +# value = device.send(HarpMessage.ReadS16(33).frame).payload_as_int() +# analog_sensor.append(value & 0xffff) +#print(f"Analog sensor's values: {analog_sensor}") # Close connection device.disconnect() diff --git a/pyharp/device.py b/pyharp/device.py index b145f9c..1c608ba 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -5,6 +5,14 @@ from pyharp.messages import HarpMessage, ReplyHarpMessage from pyharp.messages import CommonRegisters from pyharp.device_names import device_names +from enum import Enum + + +class DeviceMode(Enum): + Standby = 0 + Active = 1 + Reserved = 2 + Speed = 3 class Device: @@ -53,16 +61,13 @@ def load(self) -> None: def info(self) -> None: print("Device info:") - #print(f"* Who am I (ID): {self.WHO_AM_I}") - #print(f"* Who am I (Device): {self.WHO_AM_I_DEVICE}") print(f"* Who am I: ({self.WHO_AM_I}) {self.WHO_AM_I_DEVICE}") print(f"* HW version: {self.HW_VERSION_H}.{self.HW_VERSION_L}") print(f"* Assembly version: {self.ASSEMBLY_VERSION}") print(f"* HARP version: {self.HARP_VERSION_H}.{self.HARP_VERSION_L}") - print( - f"* Firmware version: {self.FIRMWARE_VERSION_H}.{self.FIRMWARE_VERSION_L}" - ) + print(f"* Firmware version: {self.FIRMWARE_VERSION_H}.{self.FIRMWARE_VERSION_L}") print(f"* Device user name: {self.DEVICE_NAME}") + print(f"* Mode: {self.read_device_mode().name}") def read(self): pass @@ -158,7 +163,7 @@ def read_fw_l_version(self) -> int: reply: ReplyHarpMessage = self.send( HarpMessage.ReadU8(address).frame, dump=False - ) +) return reply.payload_as_int() @@ -172,6 +177,45 @@ def read_device_name(self) -> str: return reply.payload_as_string() + def read_device_mode(self) -> int: + address = CommonRegisters.OPERATION_CTRL + reply = self.send(HarpMessage.ReadU8(address).frame) + return DeviceMode(reply.payload_as_int() & 0x03) + +# TODO: Not sure if we want to implement these. Delete if no. +# def set_mode(self, mode: DeviceMode): +# address = CommonRegisters.OPERATION_CTRL +# # Read register first. +# reg_value = self.send(HarpMessage.ReadU8(address).frame).payload_as_int() +# reg_value &= ~0x03 # mask off old mode. +# reg_value |= mode.value +# reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) +# +# def enable_alive_en(self): +# """Enable ALIVE_EN such that the device sends an event each second.""" +# address = CommonRegisters.OPERATION_CTRL +# # Read register first. +# reg_value = self.send(HarpMessage.ReadU8(address).frame).payload_as_int() +# reg_value |= (1 << 7) +# reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) +# +# def enable_status_led(self): +# """enable the device's status led if one exists.""" +# address = CommonRegisters.OPERATION_CTRL +# # Read register first. +# reg_value = self.send(HarpMessage.ReadU8(address).frame).payload_as_int() +# reg_value |= (1 << 6) +# reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) + + def disable_alive_en(self): + """disable ALIVE_EN such that the device does not send an event each second.""" + address = CommonRegisters.OPERATION_CTRL + # Read register first. + reg_value = self.send(HarpMessage.ReadU8(address).frame).payload_as_int() + reg_value &= ~(1 << 7) # mask off old mode. + reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) + + def send(self, message_bytes: bytearray, dump: bool = True) -> ReplyHarpMessage: self._ser.write(message_bytes) From 455de819097220bce69ec44459df44ae28af5bf3 Mon Sep 17 00:00:00 2001 From: Joshua Vasquez Date: Tue, 19 Apr 2022 16:14:54 -0700 Subject: [PATCH 012/267] adding simple behavior io stuff --- examples/behavior_device_driver_test.py | 41 +++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100755 examples/behavior_device_driver_test.py diff --git a/examples/behavior_device_driver_test.py b/examples/behavior_device_driver_test.py new file mode 100755 index 0000000..410584e --- /dev/null +++ b/examples/behavior_device_driver_test.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +from pyharp.drivers.behavior import Behavior +from pyharp.messages import HarpMessage +from pyharp.messages import MessageType +from struct import * +import os + + +# Open the device and print the info on screen +# Open serial connection and save communication to a file +device = None +if os.name == 'posix': # check for Linux. + device = Behavior("/dev/harp_device_00", "ibl.bin") +else: # assume Windows. + device = Behavior("COM95", "ibl.bin") + +print(f"digital outputs: {device.all_output_states:016b}") +print(f"setting digital outputs") +device.all_output_states = 0x0000 +device.set_outputs(0x0000) +print(f"digital outputs: {device.all_output_states:016b}") + +device.D0 = 1 +print(f"D0: {device.D0}") +device.D0 = 0 +print(f"D0: {device.D0}") + +device.D1 = 1 +print(f"D1: {device.D1}") +device.D1 = 0 +print(f"D1: {device.D1}") + +#import time +#while True: +# print(f"PORT0 IN State: {device.port0_i0}") +# print(f"PORT0 IO State: {device.port0_io0}") +# print(f"PORT0 OUT State: {device.port0_o0}") +# print(f"all port io states: {device.all_port_io_states}") +# print(f"all port output states: {device.all_port_output_states}") +# print() +# time.sleep(0.1) From 4e5e58fe33dc6c85006bfc2ca68c190e8ffa8666 Mon Sep 17 00:00:00 2001 From: Joshua Vasquez Date: Tue, 19 Apr 2022 16:16:22 -0700 Subject: [PATCH 013/267] adding prelim behavior driver --- pyharp/drivers/__init__.py | 0 pyharp/drivers/behavior.py | 313 +++++++++++++++++++++++++++++++++++++ 2 files changed, 313 insertions(+) create mode 100644 pyharp/drivers/__init__.py create mode 100644 pyharp/drivers/behavior.py diff --git a/pyharp/drivers/__init__.py b/pyharp/drivers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyharp/drivers/behavior.py b/pyharp/drivers/behavior.py new file mode 100644 index 0000000..1a0f6e2 --- /dev/null +++ b/pyharp/drivers/behavior.py @@ -0,0 +1,313 @@ +"""Behavior Device Driver.""" + +from pyharp.messages import HarpMessage, ReplyHarpMessage +from pyharp.device_names import device_names +from pyharp.device import Device +import serial +from serial.serialutil import SerialException + + +# Type, Base Address, "Description." +REGISTERS = \ +{ # RJ45 "PORT" (0, 1, 2) Digital Inputs + "PORT_DIS" : ("U8", 32, "Reflects the state of DI digital lines of each Port."), + + # Manipulate any of the boards digital outputs. + "OUTPUTS_SET": ("U16", 34, "Set the corresponding output."), + "OUTPUTS_CLR": ("U16", 35, "Clear the corresponding output."), + "OUTPUTS_TOGGLE": ("U16", 36, "Toggle the corresponding output."), + "OUTPUTS_OUT": ("U16", 37, "Control corresponding output."), + + # RJ45 "PORT" (0, 1, 2) Digital IOs + "PORT_DIOS_SET": ("U8", 38, "Set the corresponding DIO."), + "PORT_DIOS_CLEAR": ("U8", 39, "Clear the corresponding DIO."), + "PORT_DIOS_TOGGLE": ("U8", 40, "Toggle the corresponding DIO."), + "PORT_DIOS_OUT": ("U8", 41, "Control the corresponding DIO."), + "PORT_DIOS_CONF": ("U8", 42, "Set the DIOs direction (1 is output)."), + "PORT_DIOS_IN": ("U8", 43, "State of the DIOs."), +} + + + +class Behavior: + """Driver for BehaviorDevice.""" + + # On Linux, the symlink to the first detected harp device. + # Name set in udev rules and will increment with subsequent devices. + DEVICE_NAME = "Behavior" + DEFAULT_PORT_NAME = "/dev/harp_device_00" + ID = 1216 + + # TODO: put this in a base class? + READ_MSG_LOOKUP = \ + { + "U8": HarpMessage.ReadU8, + "U16": HarpMessage.ReadU16, + "S16": HarpMessage.ReadS16, + } + + WRITE_MSG_LOOKUP = \ + { + "U8": HarpMessage.WriteU8, + "U16": HarpMessage.WriteU16, + "S16": HarpMessage.WriteS16, + } + + + def __init__(self, port_name=None, output_filename=None): + """Class constructor. Connect to a device.""" + + self.device = None + + try: + if port_name is None: + self.device = Device(self.__class__.DEFAULT_PORT_NAME, output_filename) + else: + self.device = Device(port_name, output_filename) + except (FileNotFoundError, SerialException): + print("Error: Failed to connect to Behavior Device. Is it plugged in?") + raise + + if self.device.WHO_AM_I != self.__class__.ID: + raise IOError("Error: Did not connect to Harp Behavior Device.") + + + # TODO: put this in a base class? + def get_reg_info(self, reg_name: str) -> str: + """get info for this device's particular reg.""" + try: + return REGISTERS[reg_name][2] + except KeyError: + raise KeyError(f"reg: {reg_name} is not a register in " + "{self.__class__.name} Device's register map.") + + +# Board inputs, outputs, and some settings configured as @properties. + # INPUTS + @property + def all_port_input_states(self): + """return the state of all PORT digital inputs.""" + reg_type, reg_index, _ = REGISTERS["PORT_DIS"] + read_message_type = self.__class__.READ_MSG_LOOKUP[reg_type] + return self.device.send(read_message_type(reg_index).frame).payload_as_int() + + @property + def port0_i0(self): + """return the state of port0 digital in 0.""" + return self.all_port_input_states & 0x01 + + @property + def port1_i0(self): + """return the state of port0 digital in 0.""" + return (self.all_port_input_states >> 1) & 0x01 + + @property + def port2_i0(self): + """return the state of port2 digital in 0.""" + return (self.all_port_input_states >> 2) & 0x01 + + # IOs + def set_port_io_states(self, bitmask : int): + """set the state of all PORT digital ios. (1 is output.)""" + reg_type, reg_index, _ = REGISTERS["PORT_DIOS_CONF"] + write_message_type = self.__class__.WRITE_MSG_LOOKUP[reg_type] + self.device.send(write_message_type(reg_index, bitmask).frame) + + @property #FIXME: this doesn't seem to work + def all_port_io_states(self): + """return the state of all PORT digital ios.""" + reg_type, reg_index, _ = REGISTERS["PORT_DIOS_IN"] + read_message_type = self.__class__.READ_MSG_LOOKUP[reg_type] + return self.device.send(read_message_type(reg_index).frame).payload_as_int() + + @property + def port0_io0(self): + """read the digital io state.""" + return self.all_port_io_states & 0x01 + + @port0_io0.setter + def port0_io0(self, value: int): + """write port0 digital io state.""" + pass + + @property + def port1_io0(self): + """read the digital io state.""" + return (self.all_port_io_states >> 1) & 0x01 + + @port0_io0.setter + def port1_io0(self, value: int): + """write port0 digital io state.""" + self.set_outputs(value&0x01) + + @property + def port2_io0(self): + """read the digital io state.""" + return (self.all_port_io_states >> 2) & 0x01 + + @port0_io0.setter + def port2_io0(self, value: int): + """write port0 digital io state.""" + pass + + + # OUTPUTS + @property + def all_output_states(self): + """return the state of all PORT digital inputs.""" + reg_type, reg_index, _ = REGISTERS["OUTPUTS_OUT"] + read_message_type = self.__class__.READ_MSG_LOOKUP[reg_type] + return self.device.send(read_message_type(reg_index).frame).payload_as_int() + + @all_output_states.setter + def all_output_states(self, bitmask : int): + """set the state of all PORT digital inputs.""" + reg_type, reg_index, _ = REGISTERS["OUTPUTS_OUT"] + write_message_type = self.__class__.WRITE_MSG_LOOKUP[reg_type] + return self.device.send(write_message_type(reg_index, bitmask).frame) + + def set_outputs(self, bitmask : int): + """set digital outputs to logic 1 according to bitmask.""" + reg_type, reg_index, _ = REGISTERS["OUTPUTS_SET"] + write_message_type = self.__class__.WRITE_MSG_LOOKUP[reg_type] + return self.device.send(write_message_type(reg_index, bitmask).frame) + + def clear_outputs(self, bitmask : int): + """clear digital outputs (specified with logic 1) according to bitmask.""" + reg_type, reg_index, _ = REGISTERS["OUTPUTS_CLR"] + write_message_type = self.__class__.WRITE_MSG_LOOKUP[reg_type] + return self.device.send(write_message_type(reg_index, bitmask).frame) + + @property + def D0(self): + """read the digital output D0 state.""" + return (self.all_output_states >> 10) & 0x01 + + @D0.setter + def D0(self, value): + """set the digital output D0 state.""" + if value: + self.set_outputs(1 << 10) + else: + self.clear_outputs(1 << 10) + + @property + def D1(self): + """read the digital output D1 state.""" + return (self.all_output_states >> 11) & 0x01 + + @D1.setter + def D1(self, value): + """set the digital output D1 state.""" + if value: + self.set_outputs(1 << 11) + else: + self.clear_outputs(1 << 11) + + @property + def D2(self): + """read the digital output D2 state.""" + return (self.all_output_states >> 12) & 0x01 + + @D2.setter + def D2(self, value): + """set the digital output D2 state.""" + if value: + self.set_outputs(1 << 12) + else: + self.clear_outputs(1 << 12) + + @property + def D3(self): + """read the digital output D3 state.""" + return (self.all_output_states >> 10) & 0x01 + + @D3.setter + def D3(self, value): + """set the digital output D3 state.""" + if value: + self.set_outputs(1 << 13) + else: + self.clear_outputs(1 << 13) + + @property + def port0_D0(self): + return self.all_output_states & 0x01 + + @port0_D0.setter + def port0_D0(self, value): + if value: + self.set_outputs(1) + else: + self.clear_outputs(1) + + @property + def port1_D0(self): + return (self.all_output_states >> 1) & 0x01 + + @port1_D0.setter + def port1_D0(self, value): + if value: + self.set_outputs(1 << 1) + else: + self.clear_outputs(1 << 1) + + @property + def port2_D0(self): + return (self.all_output_states >> 2) & 0x01 + + @port2_D0.setter + def port2_D0(self, value): + if value: + self.set_outputs(1 << 2) + else: + self.clear_outputs(1 << 2) + + + @property + def port0_12V(self): + return (self.all_output_states >> 3) & 0x01 + + @port0_12V.setter + def port0_12V(self, value): + if value: + self.set_outputs(1 << 3) + else: + self.clear_outputs(1 << 3) + + @property + def port1_12V(self): + return (self.all_output_states >> 4) & 0x01 + + @port1_12V.setter + def port1_12V(self, value): + if value: + self.set_outputs(1 << 4) + else: + self.clear_outputs(1 << 4) + + @property + def port2_12V(self): + return (self.all_output_states >> 5) & 0x01 + + @port2_12V.setter + def port2_12V(self, value): + if value: + self.set_outputs(1 << 5) + else: + self.clear_outputs(1 << 5) + + + def __enter__(self): + """Setup for the 'with' statement""" + return self + + + def __exit__(self, *args): + """Cleanup for the 'with' statement""" + self.device.disconnect() + + + def __del__(self): + """Cleanup when Device gets garbage collected.""" + self.device.disconnect() From 0e47767cf9ee0c23485091d946e378d35c2f5f36 Mon Sep 17 00:00:00 2001 From: Joshua Vasquez Date: Wed, 20 Apr 2022 16:19:44 -0700 Subject: [PATCH 014/267] adding a string representation --- pyharp/messages.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pyharp/messages.py b/pyharp/messages.py index 434a134..cab9351 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -167,6 +167,16 @@ def __init__( # retrieve all content from 11 (where payload starts) until the checksum (not inclusive) self._payload = frame[11:-1] + def __str__(self): + return f"Type: {self.message_type.name}\r\n" + \ + f"Length: {self.length}\r\n" + \ + f"Address: {self.address}\r\n" + \ + f"Port: {self.port}\r\n" + \ + f"Timestampe: {self.timestamp:6f}\r\n" + \ + f"Payload Type: {self.payload_type.name}\r\n" + \ + f"Payload: {self.payload}\r\n" + \ + f"Checksum: {self.checksum}\r\n" + \ + f"Raw Frame: {self.frame}\r\n" # print(f"Type: {self.message_type.name}") # print(f"Length: {self.length}") # print(f"Address: {self.address}") From 43629c0b19aaf9eb8ccbc7095533c96769137687 Mon Sep 17 00:00:00 2001 From: Joshua Vasquez Date: Wed, 20 Apr 2022 17:55:12 -0700 Subject: [PATCH 015/267] added __str__ and active mode script --- examples/behavior_device_driver_test.py | 32 ++-- examples/wait_for_events.py | 25 +++ examples/write_and_read_from_registers.py | 11 ++ pyharp/device.py | 53 ++++--- pyharp/drivers/behavior.py | 177 +++++++++++++++------- pyharp/messages.py | 52 +++++-- 6 files changed, 249 insertions(+), 101 deletions(-) create mode 100755 examples/wait_for_events.py diff --git a/examples/behavior_device_driver_test.py b/examples/behavior_device_driver_test.py index 410584e..d9cfab7 100755 --- a/examples/behavior_device_driver_test.py +++ b/examples/behavior_device_driver_test.py @@ -14,21 +14,33 @@ else: # assume Windows. device = Behavior("COM95", "ibl.bin") +print(f"digital inputs: {device.all_input_states:03b}") print(f"digital outputs: {device.all_output_states:016b}") print(f"setting digital outputs") -device.all_output_states = 0x0000 -device.set_outputs(0x0000) +#device.all_output_states = 0x0000 # Set the whole port directly. +#device.set_outputs(0xFFFF) # Set the values set to logic 1 only. +#device.clear_outputs(0xFFFF)# Clear values set to logic 1 only. print(f"digital outputs: {device.all_output_states:016b}") +device.set_io_configuration(0b111) -device.D0 = 1 -print(f"D0: {device.D0}") -device.D0 = 0 -print(f"D0: {device.D0}") +# TODO: FIXME. IOs are not working +#device.set_io_configuration(0b111) # This is getting ignored? +#device.set_io_outputs(0b000) +#device.all_io_states = 0b000 +#print(f"digital ios: {device.all_io_states:03b}") + +#device.D0 = 0 +#print(f"D0: {device.D0}") +#device.D0 = 1 +#print(f"D0: {device.D0}") +# +#device.D1 = 0 +#print(f"D1: {device.D1}") +#device.D1 = 1 +#print(f"D1: {device.D1}") +# +#print(f"DI2: {device.DI2}") -device.D1 = 1 -print(f"D1: {device.D1}") -device.D1 = 0 -print(f"D1: {device.D1}") #import time #while True: diff --git a/examples/wait_for_events.py b/examples/wait_for_events.py new file mode 100755 index 0000000..44c2b30 --- /dev/null +++ b/examples/wait_for_events.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +from pyharp.drivers.behavior import Behavior +from pyharp.messages import HarpMessage +from pyharp.messages import MessageType +from struct import * +import os + +from pyharp.device import DeviceMode + + +# Open the device and print the info on screen +# Open serial connection and save communication to a file +device = None +if os.name == 'posix': # check for Linux. + device = Behavior("/dev/harp_device_00", "ibl.bin") +else: # assume Windows. + device = Behavior("COM95", "ibl.bin") + +print("Setting mode to active.") +device.device.set_mode(DeviceMode.Active) +import time +while True: + event_response = device.device._read() # read any incoming events. + if event_response is not None: + print(event_response) diff --git a/examples/write_and_read_from_registers.py b/examples/write_and_read_from_registers.py index 3634353..4430b00 100755 --- a/examples/write_and_read_from_registers.py +++ b/examples/write_and_read_from_registers.py @@ -25,6 +25,9 @@ #print(f"Analog sensor's higher threshold: {analog_threshold_h}") +import time + +print(f"System time: {time.perf_counter():.6f}") data_stream = device.send(HarpMessage.ReadU8(33).frame) # returns a ReplyHarpMessage #data_stream = device.send(HarpMessage.ReadS16(33).frame).payload_as_int_array() print(f"Data Stream payload type: {data_stream.payload_type.name}") @@ -33,6 +36,14 @@ print(f"Data Stream num bytes: {data_stream.length}") print(f"Data Stream payload: {data_stream.payload}") +print(f"System time: {time.perf_counter():.6f}") +event_reg_response = device.send(HarpMessage.ReadU8(77).frame) # returns a ReplyHarpMessage +print(f"EVNT_ENABLE payload type: {event_reg_response.payload_type.name}") +print(f"EVNT_ENABLE message type: {event_reg_response.message_type.name}") +print(f"EVNT_ENABLE timestamp: {event_reg_response.timestamp}") +print(f"EVNT_ENABLE num bytes: {event_reg_response.length}") +print(f"EVNT_ENABLE payload: {event_reg_response.payload[0]:08b}") + ## Increase current analog sensor's higher threshold by one unit #device.send(HarpMessage.WriteU16(42, analog_threshold_h+1).frame) # diff --git a/pyharp/device.py b/pyharp/device.py index 1c608ba..4b71f78 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -1,5 +1,5 @@ import serial -from typing import Optional +from typing import Optional, Union from pathlib import Path from pyharp.messages import HarpMessage, ReplyHarpMessage @@ -177,19 +177,22 @@ def read_device_name(self) -> str: return reply.payload_as_string() - def read_device_mode(self) -> int: + def read_device_mode(self) -> DeviceMode: address = CommonRegisters.OPERATION_CTRL reply = self.send(HarpMessage.ReadU8(address).frame) + print(reply) return DeviceMode(reply.payload_as_int() & 0x03) # TODO: Not sure if we want to implement these. Delete if no. -# def set_mode(self, mode: DeviceMode): -# address = CommonRegisters.OPERATION_CTRL -# # Read register first. -# reg_value = self.send(HarpMessage.ReadU8(address).frame).payload_as_int() -# reg_value &= ~0x03 # mask off old mode. -# reg_value |= mode.value -# reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) + def set_mode(self, mode: DeviceMode) -> ReplyHarpMessage: + """Change the device's OPMODE. Reply can be ignored.""" + address = CommonRegisters.OPERATION_CTRL + # Read register first. + reg_value = self.send(HarpMessage.ReadU8(address).frame).payload_as_int() + reg_value &= ~0x03 # mask off old mode. + reg_value |= mode.value + reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) + return reply # # def enable_alive_en(self): # """Enable ALIVE_EN such that the device sends an event each second.""" @@ -217,27 +220,35 @@ def disable_alive_en(self): def send(self, message_bytes: bytearray, dump: bool = True) -> ReplyHarpMessage: - + """Send a harp message; return the device's reply.""" self._ser.write(message_bytes) # TODO: handle case where read is None - - message_type = self._ser.read(1)[0] # byte array with only one byte - message_length = self._ser.read(1)[0] - message_content = self._ser.read(message_length) - - frame = bytearray() - frame.append(message_type) - frame.append(message_length) - frame += message_content - - reply: ReplyHarpMessage = HarpMessage.parse(frame) + reply: ReplyHarpMessage = self._read() if dump: self._dump_reply(reply.frame) return reply + + def _read(self) -> Union[ReplyHarpMessage, None]: + """Read an incoming serial message.""" + try: + message_type = self._ser.read(1)[0] # byte array with only one byte + message_length = self._ser.read(1)[0] + message_content = self._ser.read(message_length) + + frame = bytearray() + frame.append(message_type) + frame.append(message_length) + frame += message_content + + return HarpMessage.parse(frame) + except IndexError: + return None + + def _dump_reply(self, reply: bytes): assert self._dump_file_path is not None with self._dump_file_path.open(mode="ab") as f: diff --git a/pyharp/drivers/behavior.py b/pyharp/drivers/behavior.py index 1a0f6e2..77081d3 100644 --- a/pyharp/drivers/behavior.py +++ b/pyharp/drivers/behavior.py @@ -5,14 +5,16 @@ from pyharp.device import Device import serial from serial.serialutil import SerialException +from enum import Enum +# These definitions are from app_regs.h in the firmware. # Type, Base Address, "Description." REGISTERS = \ { # RJ45 "PORT" (0, 1, 2) Digital Inputs "PORT_DIS" : ("U8", 32, "Reflects the state of DI digital lines of each Port."), - # Manipulate any of the boards digital outputs. + # Manipulate any of the board's digital outputs. "OUTPUTS_SET": ("U16", 34, "Set the corresponding output."), "OUTPUTS_CLR": ("U16", 35, "Clear the corresponding output."), "OUTPUTS_TOGGLE": ("U16", 36, "Toggle the corresponding output."), @@ -25,9 +27,48 @@ "PORT_DIOS_OUT": ("U8", 41, "Control the corresponding DIO."), "PORT_DIOS_CONF": ("U8", 42, "Set the DIOs direction (1 is output)."), "PORT_DIOS_IN": ("U8", 43, "State of the DIOs."), + + "EVNT_ENABLE": ("U8", 77, "Enable events within the bitfields."), } +# Register Bitfields +class PORT_DIS(Enum): + DI0 = 0 + DI1 = 1 + DI2 = 2 + +class OUTPUTS_OUT(Enum): + PORT0_DO = 0 + PORT0_D1 = 1 + PORT0_D2 = 2 + + PORT0_12V = 3 + PORT1_12V = 4 + PORT2_12V = 5 + + B_LED0 = 6 + B_LED1 = 7 + B_RGB0 = 8 + B_RGB1 = 9 + + DO0 = 10 + DO1 = 11 + DO2 = 12 + DO3 = 13 + +class PORT_DIOS_IN(Enum): + DIO0 = 0 + DIO1 = 0 + DIO2 = 0 + +class EVNT_ENABLE(Enum): + PORT_DIS = 0 + PORT_DIOS_IN = 1 + DATA = 2 + CAM0 = 3 + CAM1 = 4 + class Behavior: """Driver for BehaviorDevice.""" @@ -72,7 +113,6 @@ def __init__(self, port_name=None, output_filename=None): raise IOError("Error: Did not connect to Harp Behavior Device.") - # TODO: put this in a base class? def get_reg_info(self, reg_name: str) -> str: """get info for this device's particular reg.""" try: @@ -85,70 +125,95 @@ def get_reg_info(self, reg_name: str) -> str: # Board inputs, outputs, and some settings configured as @properties. # INPUTS @property - def all_port_input_states(self): + def all_input_states(self): """return the state of all PORT digital inputs.""" reg_type, reg_index, _ = REGISTERS["PORT_DIS"] read_message_type = self.__class__.READ_MSG_LOOKUP[reg_type] return self.device.send(read_message_type(reg_index).frame).payload_as_int() @property - def port0_i0(self): - """return the state of port0 digital in 0.""" + def DI0(self): + """return the state of port0 digital input 0.""" return self.all_port_input_states & 0x01 @property - def port1_i0(self): - """return the state of port0 digital in 0.""" - return (self.all_port_input_states >> 1) & 0x01 - - @property - def port2_i0(self): - """return the state of port2 digital in 0.""" - return (self.all_port_input_states >> 2) & 0x01 - - # IOs - def set_port_io_states(self, bitmask : int): - """set the state of all PORT digital ios. (1 is output.)""" - reg_type, reg_index, _ = REGISTERS["PORT_DIOS_CONF"] - write_message_type = self.__class__.WRITE_MSG_LOOKUP[reg_type] - self.device.send(write_message_type(reg_index, bitmask).frame) - - @property #FIXME: this doesn't seem to work - def all_port_io_states(self): - """return the state of all PORT digital ios.""" - reg_type, reg_index, _ = REGISTERS["PORT_DIOS_IN"] - read_message_type = self.__class__.READ_MSG_LOOKUP[reg_type] - return self.device.send(read_message_type(reg_index).frame).payload_as_int() - - @property - def port0_io0(self): - """read the digital io state.""" - return self.all_port_io_states & 0x01 - - @port0_io0.setter - def port0_io0(self, value: int): - """write port0 digital io state.""" - pass - - @property - def port1_io0(self): - """read the digital io state.""" - return (self.all_port_io_states >> 1) & 0x01 - - @port0_io0.setter - def port1_io0(self, value: int): - """write port0 digital io state.""" - self.set_outputs(value&0x01) + def DI1(self): + """return the state of port1 digital input 0.""" + offset = PORT_DIS.DI1.value + return (self.all_port_input_states >> offset) & 0x01 @property - def port2_io0(self): - """read the digital io state.""" - return (self.all_port_io_states >> 2) & 0x01 - - @port0_io0.setter - def port2_io0(self, value: int): - """write port0 digital io state.""" - pass + def DI2(self): + """return the state of port2 digital input 0.""" + offset = PORT_DIS.DI2.value + return (self.all_input_states >> offset) & 0x01 + +# These do not work currently. Perhaps something needs to be cleared (MIMIC?) +# before they will configure properly. +# # IOs +# def set_io_configuration(self, bitmask : int): +# """set the state of all PORT digital ios. (1 is output.)""" +# reg_type, reg_index, _ = REGISTERS["PORT_DIOS_CONF"] +# write_message_type = self.__class__.WRITE_MSG_LOOKUP[reg_type] +# self.device.send(write_message_type(reg_index, bitmask).frame) +# +# @property +# def all_io_states(self): +# """return the state of all PORT digital ios.""" +# reg_type, reg_index, _ = REGISTERS["PORT_DIOS_IN"] +# read_message_type = self.__class__.READ_MSG_LOOKUP[reg_type] +# return self.device.send(read_message_type(reg_index).frame).payload_as_int() +# +# @all_io_states.setter +# def all_io_states(self, bitmask : int): +# """set the state of all PORT digital input/outputs.""" +# # Setting the state of the "DIO" pins, requires writing to the +# # _IN register, which is different from the OUTPUT +# reg_type, reg_index, _ = REGISTERS["PORT_DIOS_IN"] +# write_message_type = self.__class__.WRITE_MSG_LOOKUP[reg_type] +# return self.device.send(write_message_type(reg_index, bitmask).frame) +# +# def set_io_outputs(self, bitmask : int): +# """set digital input/outputs to logic 1 according to bitmask.""" +# reg_type, reg_index, _ = REGISTERS["PORT_DIOS_SET"] +# write_message_type = self.__class__.WRITE_MSG_LOOKUP[reg_type] +# return self.device.send(write_message_type(reg_index, bitmask).frame) +# +# def clear_io_outputs(self, bitmask : int): +# """clear digital input/outputs (specified with logic 1) according to bitmask.""" +# reg_type, reg_index, _ = REGISTERS["PORT_DIOS_CLEAR"] +# write_message_type = self.__class__.WRITE_MSG_LOOKUP[reg_type] +# return self.device.send(write_message_type(reg_index, bitmask).frame) +# +# @property +# def port0_io0(self): +# """read the digital io state.""" +# return self.all_port_io_states & 0x01 +# +# @port0_io0.setter +# def port0_io0(self, value: int): +# """write port0 digital io state.""" +# pass +# +# @property +# def port1_io0(self): +# """read the digital io state.""" +# return (self.all_port_io_states >> 1) & 0x01 +# +# @port0_io0.setter +# def port1_io0(self, value: int): +# """write port0 digital io state.""" +# self.set_outputs(value&0x01) +# +# @property +# def port2_io0(self): +# """read the digital io state.""" +# return (self.all_port_io_states >> 2) & 0x01 +# +# @port0_io0.setter +# def port2_io0(self, value: int): +# """write port0 digital io state.""" +# pass # OUTPUTS @@ -298,6 +363,8 @@ def port2_12V(self, value): self.clear_outputs(1 << 5) + + def __enter__(self): """Setup for the 'with' statement""" return self diff --git a/pyharp/messages.py b/pyharp/messages.py index cab9351..d1035b2 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -89,7 +89,7 @@ def frame(self) -> bytearray: return self._frame @property - def message_type(self) -> int: + def message_type(self) -> MessageType: return MessageType(self._frame[0]) @property @@ -162,29 +162,51 @@ def __init__( """ self._frame = frame - self._timestamp = int.from_bytes(frame[5:9], byteorder="little", signed=False) + \ - int.from_bytes(frame[9:11], byteorder="little", signed=False)*32e-6 # retrieve all content from 11 (where payload starts) until the checksum (not inclusive) self._payload = frame[11:-1] + # Assign timestamp after _payload since @properties all rely on self._payload. + self._timestamp = int.from_bytes(frame[5:9], byteorder="little", signed=False) + \ + int.from_bytes(frame[9:11], byteorder="little", signed=False)*32e-6 + # Timestamp is junk if it's not present. + if self.payload_type.value & PayloadType.hasTimestamp.value: + self._timestamp = None + + + def __repr__(self): + """Print debug representation of a reply message.""" + print(self.__str__()) + print(f"Raw Frame: {self.frame}") + + def __str__(self): + """Print friendly representation of a reply message.""" + payload_str = "" + format_str = "" + if self.payload_type.value & 0x01: + format_str = '08b' + elif self.payload_type.value & 0x02: + format_str = '016b' + elif self.payload_type.value & 0x04: + if self.message_type.value & PayloadType.isFloat.value: + format_str = '.6f' + else: + format_str = '032b' + elif self.payload_type.value & 0x08: + format_str = '064b' + + for item in self.payload: + payload_str += f"{item:{format_str}} " + return f"Type: {self.message_type.name}\r\n" + \ f"Length: {self.length}\r\n" + \ f"Address: {self.address}\r\n" + \ f"Port: {self.port}\r\n" + \ - f"Timestampe: {self.timestamp:6f}\r\n" + \ + f"Timestamp: {self.timestamp}\r\n" + \ f"Payload Type: {self.payload_type.name}\r\n" + \ - f"Payload: {self.payload}\r\n" + \ - f"Checksum: {self.checksum}\r\n" + \ - f"Raw Frame: {self.frame}\r\n" - # print(f"Type: {self.message_type.name}") - # print(f"Length: {self.length}") - # print(f"Address: {self.address}") - # print(f"Port: {self.port}") - # print(f"Payload Type: {self.payload_type.name}") - # print(f"Payload: {self.payload}") - # print(f"Checksum: {self.checksum}") - # print(f"Frame: {self.frame}") + f"Payload Length: {len(self.payload)}\r\n" + \ + f"Payload: {payload_str}\r\n" + \ + f"Checksum: {self.checksum}" @property def payload(self) -> bytes: From edfc3618647cf5bbe0a912dcfaaa7685d36146da Mon Sep 17 00:00:00 2001 From: Joshua Vasquez Date: Wed, 20 Apr 2022 18:09:29 -0700 Subject: [PATCH 016/267] printing DI event changes works --- examples/wait_for_events.py | 2 +- pyharp/messages.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/wait_for_events.py b/examples/wait_for_events.py index 44c2b30..291cecf 100755 --- a/examples/wait_for_events.py +++ b/examples/wait_for_events.py @@ -21,5 +21,5 @@ import time while True: event_response = device.device._read() # read any incoming events. - if event_response is not None: + if event_response is not None and event_response.address != 44: print(event_response) diff --git a/pyharp/messages.py b/pyharp/messages.py index d1035b2..da81795 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -169,7 +169,7 @@ def __init__( self._timestamp = int.from_bytes(frame[5:9], byteorder="little", signed=False) + \ int.from_bytes(frame[9:11], byteorder="little", signed=False)*32e-6 # Timestamp is junk if it's not present. - if self.payload_type.value & PayloadType.hasTimestamp.value: + if not (self.payload_type.value & PayloadType.hasTimestamp.value): self._timestamp = None From d5d835a185fe903a5b9c0426fb84a32e2b077a49 Mon Sep 17 00:00:00 2001 From: Joshua Vasquez Date: Thu, 21 Apr 2022 12:08:11 -0700 Subject: [PATCH 017/267] updating how payload is parsed --- examples/wait_for_events.py | 2 +- pyharp/device.py | 44 ++++++++++++++----------- pyharp/messages.py | 64 +++++++++++++------------------------ 3 files changed, 49 insertions(+), 61 deletions(-) diff --git a/examples/wait_for_events.py b/examples/wait_for_events.py index 291cecf..5248f00 100755 --- a/examples/wait_for_events.py +++ b/examples/wait_for_events.py @@ -18,8 +18,8 @@ print("Setting mode to active.") device.device.set_mode(DeviceMode.Active) -import time while True: event_response = device.device._read() # read any incoming events. if event_response is not None and event_response.address != 44: + print() print(event_response) diff --git a/pyharp/device.py b/pyharp/device.py index 4b71f78..5d3c7f3 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -193,29 +193,37 @@ def set_mode(self, mode: DeviceMode) -> ReplyHarpMessage: reg_value |= mode.value reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) return reply -# -# def enable_alive_en(self): -# """Enable ALIVE_EN such that the device sends an event each second.""" -# address = CommonRegisters.OPERATION_CTRL -# # Read register first. -# reg_value = self.send(HarpMessage.ReadU8(address).frame).payload_as_int() -# reg_value |= (1 << 7) -# reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) -# -# def enable_status_led(self): -# """enable the device's status led if one exists.""" -# address = CommonRegisters.OPERATION_CTRL -# # Read register first. -# reg_value = self.send(HarpMessage.ReadU8(address).frame).payload_as_int() -# reg_value |= (1 << 6) -# reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) + + def enable_status_led(self): + """enable the device's status led if one exists.""" + address = CommonRegisters.OPERATION_CTRL + # Read register first. + reg_value = self.send(HarpMessage.ReadU8(address).frame).payload_as_int() + reg_value |= (1 << 6) + reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) + + def enable_status_led(self): + """enable the device's status led if one exists.""" + address = CommonRegisters.OPERATION_CTRL + # Read register first. + reg_value = self.send(HarpMessage.ReadU8(address).frame).payload_as_int() + reg_value &= ~(1 << 6) + reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) + + def enable_alive_en(self): + """Enable ALIVE_EN such that the device sends an event each second.""" + address = CommonRegisters.OPERATION_CTRL + # Read register first. + reg_value = self.send(HarpMessage.ReadU8(address).frame).payload_as_int() + reg_value |= (1 << 7) + reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) def disable_alive_en(self): """disable ALIVE_EN such that the device does not send an event each second.""" address = CommonRegisters.OPERATION_CTRL # Read register first. - reg_value = self.send(HarpMessage.ReadU8(address).frame).payload_as_int() - reg_value &= ~(1 << 7) # mask off old mode. + reg_value = self.send(HarpMessage.ReadU8(address).frame).payload_as_fixed_int() + reg_value != (1<< 7 & 0xFF) reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) diff --git a/pyharp/messages.py b/pyharp/messages.py index da81795..2531fb2 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -39,18 +39,6 @@ class PayloadType(Enum): TimestampedFloat = hasTimestamp | Float -ALL_UNSIGNED = [PayloadType.U8, - PayloadType.U16, - PayloadType.U32, - PayloadType.TimestampedU8, - PayloadType.TimestampedU16] -ALL_SIGNED = [PayloadType.S8, - PayloadType.S16, - PayloadType.S32, - PayloadType.TimestampedS8, - PayloadType.TimestampedS16] - - class CommonRegisters: WHO_AM_I = 0x00 HW_VERSION_H = 0x01 @@ -163,7 +151,8 @@ def __init__( self._frame = frame # retrieve all content from 11 (where payload starts) until the checksum (not inclusive) - self._payload = frame[11:-1] + self._raw_payload = frame[11:-1] + self._payload = self._parse_payload(self._raw_payload) # payload formatted as list[payload type] # Assign timestamp after _payload since @properties all rely on self._payload. self._timestamp = int.from_bytes(frame[5:9], byteorder="little", signed=False) + \ @@ -173,6 +162,16 @@ def __init__( self._timestamp = None + def _parse_payload(self, raw_payload) -> list[int]: + """return the payload as a list of ints after parsing it from the raw payload.""" + is_signed = True if (self.payload_type.value & 0x80) else False + bytes_per_word = self.payload_type.value & 0x07 + payload_len = len(raw_payload) + + word_chunks = [raw_payload[i:i+bytes_per_word] for i in range(0, payload_len, bytes_per_word)] + return [int.from_bytes(chunk, byteorder="little", signed=is_signed) for chunk in word_chunks] + + def __repr__(self): """Print debug representation of a reply message.""" print(self.__str__()) @@ -183,17 +182,11 @@ def __str__(self): """Print friendly representation of a reply message.""" payload_str = "" format_str = "" - if self.payload_type.value & 0x01: - format_str = '08b' - elif self.payload_type.value & 0x02: - format_str = '016b' - elif self.payload_type.value & 0x04: - if self.message_type.value & PayloadType.isFloat.value: - format_str = '.6f' - else: - format_str = '032b' - elif self.payload_type.value & 0x08: - format_str = '064b' + if self.payload_type == PayloadType.Float: + format_str = '.6f' + else: + bytes_per_word = self.payload_type.value & 0x07 + format_str = f'0{bytes_per_word}b' for item in self.payload: payload_str += f"{item:{format_str}} " @@ -205,11 +198,12 @@ def __str__(self): f"Timestamp: {self.timestamp}\r\n" + \ f"Payload Type: {self.payload_type.name}\r\n" + \ f"Payload Length: {len(self.payload)}\r\n" + \ - f"Payload: {payload_str}\r\n" + \ + f"Payload: {self.payload}\r\n" + \ f"Checksum: {self.checksum}" @property - def payload(self) -> bytes: + def payload(self) -> Union[int, list[int]]: + """return the payload formatted as the appropriate type.""" return self._payload @property @@ -217,24 +211,10 @@ def timestamp(self) -> float: return self._timestamp def payload_as_int(self) -> int: - value: int = 0 - if self.payload_type in ALL_UNSIGNED: - value = int.from_bytes(self.payload, byteorder="little", signed=False) - elif self.payload_type in ALL_SIGNED: - value = int.from_bytes(self.payload, byteorder="little", signed=True) - return value - - def payload_as_int_array(self) -> list: - # Number of bytes per chunk. Get this from the bit field structure. - datatype_bytes = 0x0F & self.payload_type.value - # TODO: is len(self.payload) == self.length? - signed = True if self.payload_type in ALL_UNSIGNED else False - # Break the payload into chunks of datatype size in bytes - byte_chunks = [self.payload[i: i+datatype_bytes] for i in range(0, len(self.payload), datatype_bytes)] - return [int.from_bytes(chunk, byteorder="little", signed=signed) for chunk in byte_chunks] + return self.payload[0] def payload_as_string(self) -> str: - return self.payload.decode("utf-8") + return self._raw_payload.decode("utf-8") # A Read Request Message sent to a harp device. From 3a122a3dd11b5ec35a6ccb3b33b4f26c03e5c49f Mon Sep 17 00:00:00 2001 From: Joshua Vasquez Date: Thu, 21 Apr 2022 17:17:57 -0700 Subject: [PATCH 018/267] adding enabling of events --- examples/wait_for_events.py | 6 +++-- pyharp/device.py | 10 ++++---- pyharp/drivers/behavior.py | 46 +++++++++++++++++++++++++++++-------- pyharp/messages.py | 2 +- 4 files changed, 46 insertions(+), 18 deletions(-) diff --git a/examples/wait_for_events.py b/examples/wait_for_events.py index 5248f00..8f1b9f8 100755 --- a/examples/wait_for_events.py +++ b/examples/wait_for_events.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -from pyharp.drivers.behavior import Behavior +from pyharp.drivers.behavior import Behavior, Events from pyharp.messages import HarpMessage from pyharp.messages import MessageType from struct import * @@ -18,8 +18,10 @@ print("Setting mode to active.") device.device.set_mode(DeviceMode.Active) +device.disable_all_events() +device.enable_events(Events.port_digital_inputs) while True: event_response = device.device._read() # read any incoming events. - if event_response is not None and event_response.address != 44: + if event_response is not None:# and event_response.address != 44: print() print(event_response) diff --git a/pyharp/device.py b/pyharp/device.py index 5d3c7f3..8eeb366 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -32,10 +32,6 @@ class Device: HARP_VERSION_L: int FIRMWARE_VERSION_H: int FIRMWARE_VERSION_L: int - # TIMESTAMP_SECOND = 0x08 - # TIMESTAMP_MICRO = 0x09 - # OPERATION_CTRL = 0x0A - # RESET_DEV = 0x0B DEVICE_NAME: str def __init__(self, serial_port: str, dump_file_path: Optional[str] = None): @@ -222,8 +218,8 @@ def disable_alive_en(self): """disable ALIVE_EN such that the device does not send an event each second.""" address = CommonRegisters.OPERATION_CTRL # Read register first. - reg_value = self.send(HarpMessage.ReadU8(address).frame).payload_as_fixed_int() - reg_value != (1<< 7 & 0xFF) + reg_value = self.send(HarpMessage.ReadU8(address).frame).payload[0] + reg_value &= ((1<< 7) ^ 0xFF) # bitwise ~ operator substitute for Python ints. reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) @@ -232,6 +228,8 @@ def send(self, message_bytes: bytearray, dump: bool = True) -> ReplyHarpMessage: self._ser.write(message_bytes) # TODO: handle case where read is None + # FIXME: waiting for a message reply like this + # breaks if events are also being broadcasted (i.e: in ActiveMode). reply: ReplyHarpMessage = self._read() if dump: diff --git a/pyharp/drivers/behavior.py b/pyharp/drivers/behavior.py index 77081d3..989f89a 100644 --- a/pyharp/drivers/behavior.py +++ b/pyharp/drivers/behavior.py @@ -28,6 +28,8 @@ "PORT_DIOS_CONF": ("U8", 42, "Set the DIOs direction (1 is output)."), "PORT_DIOS_IN": ("U8", 43, "State of the DIOs."), + "ADD_REG_DATA": ("S16", 44, "Voltage at ADC input and decoder (poke2) value."), + "EVNT_ENABLE": ("U8", 77, "Enable events within the bitfields."), } @@ -62,16 +64,18 @@ class PORT_DIOS_IN(Enum): DIO1 = 0 DIO2 = 0 -class EVNT_ENABLE(Enum): - PORT_DIS = 0 - PORT_DIOS_IN = 1 - DATA = 2 - CAM0 = 3 - CAM1 = 4 + +# reader-friendly events for enabling/disabling. +class Events(Enum): + port_digital_inputs = 0 # PORT_DIS + port_digital_ios = 1 # PORT_DIOS_IN + analog_input = 2 # DATA + cam0 = 3 # CAM0 + cam1 = 3 # CAM1 class Behavior: - """Driver for BehaviorDevice.""" + """Driver for Behavior Device.""" # On Linux, the symlink to the first detected harp device. # Name set in udev rules and will increment with subsequent devices. @@ -122,6 +126,28 @@ def get_reg_info(self, reg_name: str) -> str: "{self.__class__.name} Device's register map.") + def disable_all_events(self) -> ReplyHarpMessage: + """Disable the publishing of all events from Behavior device.""" + event_reg_bitmask = (((1 << Events.port_digital_inputs.value) | \ + (1 << Events.port_digital_ios.value) | \ + (1 << Events.analog_input.value) | \ + (1 << Events.cam0.value) | \ + (1 << Events.cam1.value) ) ^ 0xFF) + reg_type, reg_index, _ = REGISTERS["EVNT_ENABLE"] + write_message_type = self.__class__.WRITE_MSG_LOOKUP[reg_type] + return self.device.send(write_message_type(reg_index, event_reg_bitmask).frame) + + + def enable_events(self, *events: Events) -> ReplyHarpMessage: + """enable any events passed in as arguments.""" + event_reg_bitmask = 0x00 + for event in events: + event_reg_bitmask |= (1 << event.value) + reg_type, reg_index, _ = REGISTERS["EVNT_ENABLE"] + write_message_type = self.__class__.WRITE_MSG_LOOKUP[reg_type] + return self.device.send(write_message_type(reg_index, event_reg_bitmask).frame) + + # Board inputs, outputs, and some settings configured as @properties. # INPUTS @property @@ -372,9 +398,11 @@ def __enter__(self): def __exit__(self, *args): """Cleanup for the 'with' statement""" - self.device.disconnect() + if self.device is not None: + self.device.disconnect() def __del__(self): """Cleanup when Device gets garbage collected.""" - self.device.disconnect() + if self.device is not None: + self.device.disconnect() diff --git a/pyharp/messages.py b/pyharp/messages.py index 2531fb2..e458758 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -166,7 +166,7 @@ def _parse_payload(self, raw_payload) -> list[int]: """return the payload as a list of ints after parsing it from the raw payload.""" is_signed = True if (self.payload_type.value & 0x80) else False bytes_per_word = self.payload_type.value & 0x07 - payload_len = len(raw_payload) + payload_len = len(raw_payload) # payload length in bytes. word_chunks = [raw_payload[i:i+bytes_per_word] for i in range(0, payload_len, bytes_per_word)] return [int.from_bytes(chunk, byteorder="little", signed=is_signed) for chunk in word_chunks] From 2fc5c5866a51a4d29b19bde5339ea910672b8fed Mon Sep 17 00:00:00 2001 From: Joshua Vasquez Date: Fri, 22 Apr 2022 12:55:00 -0700 Subject: [PATCH 019/267] adding inWaiting to reads --- examples/wait_for_events.py | 1 + pyharp/device.py | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/wait_for_events.py b/examples/wait_for_events.py index 8f1b9f8..18c8d29 100755 --- a/examples/wait_for_events.py +++ b/examples/wait_for_events.py @@ -17,6 +17,7 @@ device = Behavior("COM95", "ibl.bin") print("Setting mode to active.") +# Mode will remain active for up to 3 seconds after CTS pin is brought low. device.device.set_mode(DeviceMode.Active) device.disable_all_events() device.enable_events(Events.port_digital_inputs) diff --git a/pyharp/device.py b/pyharp/device.py index 8eeb366..2bae568 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -239,7 +239,11 @@ def send(self, message_bytes: bytearray, dump: bool = True) -> ReplyHarpMessage: def _read(self) -> Union[ReplyHarpMessage, None]: - """Read an incoming serial message.""" + """(Blocking) Read an incoming serial message.""" + # block until we get at least one byte. + while True: + if self._ser.inWaiting(): + break try: message_type = self._ser.read(1)[0] # byte array with only one byte message_length = self._ser.read(1)[0] From 3f7dfba5e12ef84842351f9408c5eaa6f1256a6b Mon Sep 17 00:00:00 2001 From: Joshua Vasquez Date: Thu, 28 Sep 2023 14:24:13 -0700 Subject: [PATCH 020/267] read dumped registers --- examples/get_info.py | 6 +++++- pyharp/device.py | 51 ++++++++++++++++++++++++++++++++++++++------ pyharp/messages.py | 3 +-- 3 files changed, 50 insertions(+), 10 deletions(-) diff --git a/examples/get_info.py b/examples/get_info.py index 44e4f18..ebb202c 100755 --- a/examples/get_info.py +++ b/examples/get_info.py @@ -15,7 +15,9 @@ # Open the device and print the info on screen # Open serial connection and save communication to a file if os.name == 'posix': # check for Linux. - device = Device("/dev/harp_device_00", "ibl.bin") + #device = Device("/dev/harp_device_00", "ibl.bin") + #device = Device("/dev/ttyACM0") + device = Device("/dev/ttyUSB0") else: # assume Windows. device = Device("COM95", "ibl.bin") device.info() # Display device's info on screen @@ -34,5 +36,7 @@ device_harp_l = device.HARP_VERSION_L # Get device's harp core version device_assembly = device.ASSEMBLY_VERSION # Get device's assembly version +print(device.dump_registers()) + # Close connection device.disconnect() diff --git a/pyharp/device.py b/pyharp/device.py index 2bae568..9278273 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -3,9 +3,10 @@ from pathlib import Path from pyharp.messages import HarpMessage, ReplyHarpMessage -from pyharp.messages import CommonRegisters +from pyharp.messages import CommonRegisters, MessageType from pyharp.device_names import device_names from enum import Enum +from time import perf_counter class DeviceMode(Enum): @@ -34,6 +35,8 @@ class Device: FIRMWARE_VERSION_L: int DEVICE_NAME: str + TIMEOUT_S = 1.0 + def __init__(self, serial_port: str, dump_file_path: Optional[str] = None): self._serial_port = serial_port if dump_file_path is None: @@ -72,7 +75,7 @@ def connect(self) -> None: self._ser = serial.Serial( self._serial_port, # "/dev/tty.usbserial-A106C8O9" baudrate=1000000, - timeout=1, + timeout=self.__class__.TIMEOUT_S, parity=serial.PARITY_NONE, stopbits=1, bytesize=8, @@ -88,7 +91,7 @@ def read_who_am_i(self) -> int: reply: ReplyHarpMessage = self.send( HarpMessage.ReadU16(address).frame, dump=False ) - + #print(str(reply)) return reply.payload_as_int() def read_who_am_i_device(self) -> str: @@ -97,6 +100,7 @@ def read_who_am_i_device(self) -> str: reply: ReplyHarpMessage = self.send( HarpMessage.ReadU16(address).frame, dump=False ) + #print(str(reply)) return device_names.get(reply.payload_as_int()) @@ -106,6 +110,7 @@ def read_hw_version_h(self) -> int: reply: ReplyHarpMessage = self.send( HarpMessage.ReadU8(address).frame, dump=False ) + #print(str(reply)) return reply.payload_as_int() @@ -115,6 +120,7 @@ def read_hw_version_l(self) -> int: reply: ReplyHarpMessage = self.send( HarpMessage.ReadU8(address).frame, dump=False ) + #print(str(reply)) return reply.payload_as_int() @@ -124,6 +130,7 @@ def read_assembly_version(self) -> int: reply: ReplyHarpMessage = self.send( HarpMessage.ReadU8(address).frame, dump=False ) + #print(str(reply)) return reply.payload_as_int() @@ -133,6 +140,7 @@ def read_harp_h_version(self) -> int: reply: ReplyHarpMessage = self.send( HarpMessage.ReadU8(address).frame, dump=False ) + #print(str(reply)) return reply.payload_as_int() @@ -142,6 +150,7 @@ def read_harp_l_version(self) -> int: reply: ReplyHarpMessage = self.send( HarpMessage.ReadU8(address).frame, dump=False ) + #print(str(reply)) return reply.payload_as_int() @@ -151,6 +160,7 @@ def read_fw_h_version(self) -> int: reply: ReplyHarpMessage = self.send( HarpMessage.ReadU8(address).frame, dump=False ) + #print(str(reply)) return reply.payload_as_int() @@ -160,6 +170,7 @@ def read_fw_l_version(self) -> int: reply: ReplyHarpMessage = self.send( HarpMessage.ReadU8(address).frame, dump=False ) + #print(str(reply)) return reply.payload_as_int() @@ -170,15 +181,32 @@ def read_device_name(self) -> str: reply: ReplyHarpMessage = self.send( HarpMessage.ReadU8(address).frame, dump=False ) + #print(str(reply)) return reply.payload_as_string() def read_device_mode(self) -> DeviceMode: address = CommonRegisters.OPERATION_CTRL reply = self.send(HarpMessage.ReadU8(address).frame) - print(reply) return DeviceMode(reply.payload_as_int() & 0x03) + def dump_registers(self) -> list: + """Assert the DUMP bit to dump the values of all core and app registers + as Harp Read Reply Messages. + """ + address = CommonRegisters.OPERATION_CTRL + reg_value = self.send(HarpMessage.ReadU8(address).frame).payload_as_int() + reg_value |= 0x08 # Assert DUMP bit + self._ser.write(HarpMessage.WriteU8(address, reg_value).frame) + replies = [] + while True: + msg = self._read() + if msg is not None: + replies.append(msg) + else: + break + return replies + # TODO: Not sure if we want to implement these. Delete if no. def set_mode(self, mode: DeviceMode) -> ReplyHarpMessage: """Change the device's OPMODE. Reply can be ignored.""" @@ -225,6 +253,7 @@ def disable_alive_en(self): def send(self, message_bytes: bytearray, dump: bool = True) -> ReplyHarpMessage: """Send a harp message; return the device's reply.""" + #print(f"Sending: {repr(message_bytes)}") self._ser.write(message_bytes) # TODO: handle case where read is None @@ -232,7 +261,7 @@ def send(self, message_bytes: bytearray, dump: bool = True) -> ReplyHarpMessage: # breaks if events are also being broadcasted (i.e: in ActiveMode). reply: ReplyHarpMessage = self._read() - if dump: + if dump and self._dump_file_path is not None: self._dump_reply(reply.frame) return reply @@ -240,21 +269,29 @@ def send(self, message_bytes: bytearray, dump: bool = True) -> ReplyHarpMessage: def _read(self) -> Union[ReplyHarpMessage, None]: """(Blocking) Read an incoming serial message.""" - # block until we get at least one byte. + # block for up to TIMEOUT until we get at least one byte. + read_start = perf_counter() while True: if self._ser.inWaiting(): break + if perf_counter() - read_start >= self.__class__.TIMEOUT_S: + break try: message_type = self._ser.read(1)[0] # byte array with only one byte message_length = self._ser.read(1)[0] message_content = self._ser.read(message_length) + #print(f"Read back:") + #print(f" type: {MessageType(message_type).name}") + #print(f" length : {repr(message_length)}") + #print(f" payload: {list(message_content)}") frame = bytearray() frame.append(message_type) frame.append(message_length) frame += message_content + msg = HarpMessage.parse(frame) - return HarpMessage.parse(frame) + return msg except IndexError: return None diff --git a/pyharp/messages.py b/pyharp/messages.py index e458758..71d0199 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -174,8 +174,7 @@ def _parse_payload(self, raw_payload) -> list[int]: def __repr__(self): """Print debug representation of a reply message.""" - print(self.__str__()) - print(f"Raw Frame: {self.frame}") + return self.__str__() + f"\r\nRaw Frame: {self.frame}" def __str__(self): From 401bd36a77c687698e38d4faae36a3801d6b70ce Mon Sep 17 00:00:00 2001 From: Joshua Vasquez Date: Thu, 21 Dec 2023 13:57:34 -0800 Subject: [PATCH 021/267] expose more core features --- examples/get_info.py | 8 +++--- examples/wait_for_events.py | 13 +++++----- pyharp/device.py | 8 +++--- pyharp/messages.py | 51 ++++++++++++++++++++++++++++++++++--- 4 files changed, 64 insertions(+), 16 deletions(-) diff --git a/examples/get_info.py b/examples/get_info.py index ebb202c..f2d8e5e 100755 --- a/examples/get_info.py +++ b/examples/get_info.py @@ -16,8 +16,8 @@ # Open serial connection and save communication to a file if os.name == 'posix': # check for Linux. #device = Device("/dev/harp_device_00", "ibl.bin") - #device = Device("/dev/ttyACM0") - device = Device("/dev/ttyUSB0") + device = Device("/dev/ttyACM0") + #device = Device("/dev/ttyUSB0") else: # assume Windows. device = Device("COM95", "ibl.bin") device.info() # Display device's info on screen @@ -36,7 +36,9 @@ device_harp_l = device.HARP_VERSION_L # Get device's harp core version device_assembly = device.ASSEMBLY_VERSION # Get device's assembly version -print(device.dump_registers()) +reg_dump = device.dump_registers() +for i in range(11): + print(reg_dump[i]) # Close connection device.disconnect() diff --git a/examples/wait_for_events.py b/examples/wait_for_events.py index 18c8d29..845e7c4 100755 --- a/examples/wait_for_events.py +++ b/examples/wait_for_events.py @@ -5,24 +5,25 @@ from struct import * import os -from pyharp.device import DeviceMode +from pyharp.device import Device, DeviceMode # Open the device and print the info on screen # Open serial connection and save communication to a file device = None if os.name == 'posix': # check for Linux. - device = Behavior("/dev/harp_device_00", "ibl.bin") + #device = Behavior("/dev/harp_device_00", "ibl.bin") + device = Device("/dev/ttyACM0",) else: # assume Windows. device = Behavior("COM95", "ibl.bin") print("Setting mode to active.") # Mode will remain active for up to 3 seconds after CTS pin is brought low. -device.device.set_mode(DeviceMode.Active) -device.disable_all_events() -device.enable_events(Events.port_digital_inputs) +device.set_mode(DeviceMode.Active) +#device.disable_all_events() +#device.enable_events(Events.port_digital_inputs) while True: - event_response = device.device._read() # read any incoming events. + event_response = device._read() # read any incoming events. if event_response is not None:# and event_response.address != 44: print() print(event_response) diff --git a/pyharp/device.py b/pyharp/device.py index 9278273..166ce2f 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -223,15 +223,15 @@ def enable_status_led(self): address = CommonRegisters.OPERATION_CTRL # Read register first. reg_value = self.send(HarpMessage.ReadU8(address).frame).payload_as_int() - reg_value |= (1 << 6) + reg_value |= (1 << 5) reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) - def enable_status_led(self): - """enable the device's status led if one exists.""" + def disable_status_led(self): + """disable the device's status led if one exists.""" address = CommonRegisters.OPERATION_CTRL # Read register first. reg_value = self.send(HarpMessage.ReadU8(address).frame).payload_as_int() - reg_value &= ~(1 << 6) + reg_value &= ~(1 << 5) reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) def enable_alive_en(self): diff --git a/pyharp/messages.py b/pyharp/messages.py index 71d0199..7566e79 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -2,6 +2,7 @@ from enum import Enum # from abc import ABC, abstractmethod from typing import Union, Tuple, Optional +import struct class MessageType(Enum): @@ -93,7 +94,7 @@ def port(self) -> int: return self._frame[3] @property - def payload_type(self) -> int: + def payload_type(self) -> PayloadType: return PayloadType(self._frame[4]) @property @@ -116,6 +117,16 @@ def ReadS16(address: int) -> ReadS16HarpMessage: def ReadU16(address: int) -> ReadU16HarpMessage: return ReadU16HarpMessage(address) + # TODO: ReadS16 + + @staticmethod + def ReadU32(address: int) -> ReadU32HarpMessage: + return ReadU32HarpMessage(address) + + @staticmethod + def ReadFloat(address: int) -> ReadFloatHarpMessage: + return ReadFloatHarpMessage(address) + @staticmethod def WriteU8(address: int, value: int) -> WriteU8HarpMessage: return WriteU8HarpMessage(address, value) @@ -132,6 +143,10 @@ def WriteS16(address: int, value: int) -> WriteS16HarpMessage: def WriteU16(address: int, value: int) -> WriteU16HarpMessage: return WriteU16HarpMessage(address, value) + @staticmethod + def WriteFloat(address: int, value: int) -> WriteFloatHarpMessage: + return WriteFloatHarpMessage(address, value) + @staticmethod def parse(frame: bytearray) -> ReplyHarpMessage: return ReplyHarpMessage(frame) @@ -165,11 +180,15 @@ def __init__( def _parse_payload(self, raw_payload) -> list[int]: """return the payload as a list of ints after parsing it from the raw payload.""" is_signed = True if (self.payload_type.value & 0x80) else False + is_float = True if (self.payload_type.value & 0x40) else False bytes_per_word = self.payload_type.value & 0x07 payload_len = len(raw_payload) # payload length in bytes. word_chunks = [raw_payload[i:i+bytes_per_word] for i in range(0, payload_len, bytes_per_word)] - return [int.from_bytes(chunk, byteorder="little", signed=is_signed) for chunk in word_chunks] + if not is_float: + return [int.from_bytes(chunk, byteorder="little", signed=is_signed) for chunk in word_chunks] + else: # handle float case. + return [struct.unpack(' int: def payload_as_string(self) -> str: return self._raw_payload.decode("utf-8") + def payload_as_float(self) -> float: + return self.payload[0] # already parsed. + # A Read Request Message sent to a harp device. class ReadHarpMessage(HarpMessage): @@ -253,6 +275,15 @@ class ReadS16HarpMessage(ReadHarpMessage): def __init__(self, address: int): super().__init__(PayloadType.S16, address) +class ReadU32HarpMessage(ReadHarpMessage): + def __init__(self, address: int): + super().__init__(PayloadType.U32, address) + + +class ReadFloatHarpMessage(ReadHarpMessage): + def __init__(self, address: int): + super().__init__(PayloadType.Float, address) + class WriteHarpMessage(HarpMessage): BASE_LENGTH: int = 5 @@ -327,3 +358,17 @@ def __init__(self, address: int, value: int): @property def payload(self) -> int: return int.from_bytes(self._frame[5:7], byteorder="little", signed=True) + + +class WriteFloatHarpMessage(WriteHarpMessage): + def __init__(self, address: int, value: float): + super().__init__( + PayloadType.Float, + struct.pack(' float: + return struct.unpack(' Date: Thu, 21 Dec 2023 14:53:21 -0800 Subject: [PATCH 022/267] add WriteU32 and WriteS32 --- examples/get_info.py | 4 ++-- examples/wait_for_events.py | 3 ++- pyharp/messages.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/examples/get_info.py b/examples/get_info.py index f2d8e5e..d7b7a47 100755 --- a/examples/get_info.py +++ b/examples/get_info.py @@ -16,8 +16,8 @@ # Open serial connection and save communication to a file if os.name == 'posix': # check for Linux. #device = Device("/dev/harp_device_00", "ibl.bin") - device = Device("/dev/ttyACM0") - #device = Device("/dev/ttyUSB0") + #device = Device("/dev/ttyACM0") + device = Device("/dev/ttyUSB0") else: # assume Windows. device = Device("COM95", "ibl.bin") device.info() # Display device's info on screen diff --git a/examples/wait_for_events.py b/examples/wait_for_events.py index 845e7c4..b846d37 100755 --- a/examples/wait_for_events.py +++ b/examples/wait_for_events.py @@ -13,7 +13,8 @@ device = None if os.name == 'posix': # check for Linux. #device = Behavior("/dev/harp_device_00", "ibl.bin") - device = Device("/dev/ttyACM0",) + #device = Device("/dev/ttyACM0",) + device = Device("/dev/ttyUSB0",) else: # assume Windows. device = Behavior("COM95", "ibl.bin") diff --git a/pyharp/messages.py b/pyharp/messages.py index 7566e79..b7bd57d 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -147,6 +147,14 @@ def WriteU16(address: int, value: int) -> WriteU16HarpMessage: def WriteFloat(address: int, value: int) -> WriteFloatHarpMessage: return WriteFloatHarpMessage(address, value) + @staticmethod + def WriteU32(address: int, value: int) -> WriteU32HarpMessage: + return WriteU32HarpMessage(address, value) + + @staticmethod + def WriteS32(address: int, value: int) -> WriteS32HarpMessage: + return WriteS32HarpMessage(address, value) + @staticmethod def parse(frame: bytearray) -> ReplyHarpMessage: return ReplyHarpMessage(frame) @@ -372,3 +380,25 @@ def __init__(self, address: int, value: float): @property def payload(self) -> float: return struct.unpack(' int: + return int.from_bytes(self._frame[5:9], byteorder="little", signed=False) + + +class WriteS32HarpMessage(WriteHarpMessage): + def __init__(self, address: int, value: int): + super().__init__( + PayloadType.S32, value.to_bytes(4, byteorder="little", signed=False), address, offset=3 + ) + + @property + def payload(self) -> int: + return int.from_bytes(self._frame[5:9], byteorder="little", signed=False) From fb5447628e542f2ec7ff693efe8aed6b5406005a Mon Sep 17 00:00:00 2001 From: Joshua Vasquez Date: Thu, 21 Dec 2023 14:55:01 -0800 Subject: [PATCH 023/267] fix signed error in WriteS32 --- pyharp/messages.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyharp/messages.py b/pyharp/messages.py index b7bd57d..3d34af4 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -396,9 +396,9 @@ def payload(self) -> int: class WriteS32HarpMessage(WriteHarpMessage): def __init__(self, address: int, value: int): super().__init__( - PayloadType.S32, value.to_bytes(4, byteorder="little", signed=False), address, offset=3 + PayloadType.S32, value.to_bytes(4, byteorder="little", signed=True), address, offset=3 ) @property def payload(self) -> int: - return int.from_bytes(self._frame[5:9], byteorder="little", signed=False) + return int.from_bytes(self._frame[5:9], byteorder="little", signed=True) From e7900d41f8bc1a9f6a246fbf9768456466308479 Mon Sep 17 00:00:00 2001 From: Joshua Vasquez Date: Fri, 12 Jan 2024 14:57:43 -0800 Subject: [PATCH 024/267] add debug level logging. --- pyharp/device.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyharp/device.py b/pyharp/device.py index 166ce2f..547639f 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -1,4 +1,5 @@ import serial +import logging from typing import Optional, Union from pathlib import Path @@ -38,6 +39,7 @@ class Device: TIMEOUT_S = 1.0 def __init__(self, serial_port: str, dump_file_path: Optional[str] = None): + self.log = logging.getLogger(f"{__name__}.{self.__class__.__name__}") self._serial_port = serial_port if dump_file_path is None: self._dump_file_path = None @@ -280,6 +282,9 @@ def _read(self) -> Union[ReplyHarpMessage, None]: message_type = self._ser.read(1)[0] # byte array with only one byte message_length = self._ser.read(1)[0] message_content = self._ser.read(message_length) + self.log.debug(f"reply (type): {message_type}") + self.log.debug(f"reply (length): {message_length}") + self.log.debug(f"reply (payload): {message_content}") #print(f"Read back:") #print(f" type: {MessageType(message_type).name}") #print(f" length : {repr(message_length)}") From 1b1e833e2ee750471c94623f28fc499a6134c314 Mon Sep 17 00:00:00 2001 From: "jessy.liao" Date: Fri, 17 May 2024 14:54:57 -0700 Subject: [PATCH 025/267] Added ReadS32 and refactored WriteS32 to handle array inputs --- pyharp/messages.py | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/pyharp/messages.py b/pyharp/messages.py index 3d34af4..e86747a 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -1,7 +1,7 @@ from __future__ import annotations # for type hints (PEP 563) from enum import Enum # from abc import ABC, abstractmethod -from typing import Union, Tuple, Optional +from typing import Union, Tuple, Optional, List import struct @@ -123,6 +123,10 @@ def ReadU16(address: int) -> ReadU16HarpMessage: def ReadU32(address: int) -> ReadU32HarpMessage: return ReadU32HarpMessage(address) + @staticmethod + def ReadS32(address: int) -> ReadS32HarpMessage: + return ReadS32HarpMessage(address) + @staticmethod def ReadFloat(address: int) -> ReadFloatHarpMessage: return ReadFloatHarpMessage(address) @@ -288,6 +292,11 @@ def __init__(self, address: int): super().__init__(PayloadType.U32, address) +class ReadS32HarpMessage(ReadHarpMessage): + def __init__(self, address: int): + super().__init__(PayloadType.S32, address) + + class ReadFloatHarpMessage(ReadHarpMessage): def __init__(self, address: int): super().__init__(PayloadType.Float, address) @@ -317,8 +326,12 @@ def __init__( self._frame.append(HarpMessage.DEFAULT_PORT) self._frame.append(payload_type.value) - for i in payload: - self._frame.append(i) + # Handle payloads that are bytes or bytearray (bytearray = multi-motor instructions) + if isinstance(payload, bytearray): + self._frame += payload + else: + for i in payload: + self._frame.append(i) self._frame.append(self.calculate_checksum()) @@ -394,11 +407,19 @@ def payload(self) -> int: class WriteS32HarpMessage(WriteHarpMessage): - def __init__(self, address: int, value: int): + def __init__(self, address: int, value: int | List[int]): + if isinstance(value, list): + payload = bytearray() + for val in value: + payload += val.to_bytes(4, byteorder="little", signed=True) + offset = 15 + else: + payload = value.to_bytes(4, byteorder="little", signed=True) + offset = 3 super().__init__( - PayloadType.S32, value.to_bytes(4, byteorder="little", signed=True), address, offset=3 + PayloadType.S32, payload, address, offset=offset ) @property - def payload(self) -> int: + def payload(self) -> int | List[int]: return int.from_bytes(self._frame[5:9], byteorder="little", signed=True) From 368e346dd66c17516974a339a0cf3f4807c02874 Mon Sep 17 00:00:00 2001 From: Patrick Latimer Date: Wed, 30 Oct 2024 16:23:22 -0700 Subject: [PATCH 026/267] Add threaded serial port --- pyharp/device.py | 171 ++++++++++++++++++++++++------------------ pyharp/harp_serial.py | 84 +++++++++++++++++++++ pyharp/messages.py | 95 ++++++++++++++--------- pyproject.toml | 1 + tests/test_device.py | 27 ++++++- 5 files changed, 269 insertions(+), 109 deletions(-) create mode 100644 pyharp/harp_serial.py diff --git a/pyharp/device.py b/pyharp/device.py index 547639f..4496859 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -1,9 +1,18 @@ import serial import logging +import threading +import queue from typing import Optional, Union from pathlib import Path -from pyharp.messages import HarpMessage, ReplyHarpMessage +from pyharp.harp_serial import HarpSerial +from pyharp.messages import ( + HarpMessage, + ReadHarpMessage, + ReplyHarpMessage, + # ResetDevOffsets, + Register, +) from pyharp.messages import CommonRegisters, MessageType from pyharp.device_names import device_names from enum import Enum @@ -22,7 +31,8 @@ class Device: https://github.com/harp-tech/protocol/blob/master/Device%201.1%201.0%2020220402.pdf """ - _ser: serial.Serial + # _ser: serial.Serial + _ser: HarpSerial _dump_file_path: Path WHO_AM_I: int @@ -38,13 +48,19 @@ class Device: TIMEOUT_S = 1.0 - def __init__(self, serial_port: str, dump_file_path: Optional[str] = None): + def __init__( + self, + serial_port: str, + dump_file_path: Optional[str] = None, + read_timeout_s=1, + ): self.log = logging.getLogger(f"{__name__}.{self.__class__.__name__}") self._serial_port = serial_port if dump_file_path is None: self._dump_file_path = None else: self._dump_file_path = Path() / dump_file_path + self.read_timeout_s = read_timeout_s self.connect() self.load() @@ -74,7 +90,8 @@ def read(self): pass def connect(self) -> None: - self._ser = serial.Serial( + self._ser = HarpSerial( + # self._ser = serial.Serial( self._serial_port, # "/dev/tty.usbserial-A106C8O9" baudrate=1000000, timeout=self.__class__.TIMEOUT_S, @@ -93,7 +110,7 @@ def read_who_am_i(self) -> int: reply: ReplyHarpMessage = self.send( HarpMessage.ReadU16(address).frame, dump=False ) - #print(str(reply)) + # print(str(reply)) return reply.payload_as_int() def read_who_am_i_device(self) -> str: @@ -102,77 +119,63 @@ def read_who_am_i_device(self) -> str: reply: ReplyHarpMessage = self.send( HarpMessage.ReadU16(address).frame, dump=False ) - #print(str(reply)) + # print(str(reply)) return device_names.get(reply.payload_as_int()) def read_hw_version_h(self) -> int: address = CommonRegisters.HW_VERSION_H - reply: ReplyHarpMessage = self.send( - HarpMessage.ReadU8(address).frame, dump=False - ) - #print(str(reply)) + reply: ReplyHarpMessage = self.send(HarpMessage.ReadU8(address).frame, dump=False) + # print(str(reply)) return reply.payload_as_int() def read_hw_version_l(self) -> int: address = CommonRegisters.HW_VERSION_L - reply: ReplyHarpMessage = self.send( - HarpMessage.ReadU8(address).frame, dump=False - ) - #print(str(reply)) + reply: ReplyHarpMessage = self.send(HarpMessage.ReadU8(address).frame, dump=False) + # print(str(reply)) return reply.payload_as_int() def read_assembly_version(self) -> int: address = CommonRegisters.ASSEMBLY_VERSION - reply: ReplyHarpMessage = self.send( - HarpMessage.ReadU8(address).frame, dump=False - ) - #print(str(reply)) + reply: ReplyHarpMessage = self.send(HarpMessage.ReadU8(address).frame, dump=False) + # print(str(reply)) return reply.payload_as_int() def read_harp_h_version(self) -> int: address = CommonRegisters.HARP_VERSION_H - reply: ReplyHarpMessage = self.send( - HarpMessage.ReadU8(address).frame, dump=False - ) - #print(str(reply)) + reply: ReplyHarpMessage = self.send(HarpMessage.ReadU8(address).frame, dump=False) + # print(str(reply)) return reply.payload_as_int() def read_harp_l_version(self) -> int: address = CommonRegisters.HARP_VERSION_L - reply: ReplyHarpMessage = self.send( - HarpMessage.ReadU8(address).frame, dump=False - ) - #print(str(reply)) + reply: ReplyHarpMessage = self.send(HarpMessage.ReadU8(address).frame, dump=False) + # print(str(reply)) return reply.payload_as_int() def read_fw_h_version(self) -> int: address = CommonRegisters.FIRMWARE_VERSION_H - reply: ReplyHarpMessage = self.send( - HarpMessage.ReadU8(address).frame, dump=False - ) - #print(str(reply)) + reply: ReplyHarpMessage = self.send(HarpMessage.ReadU8(address).frame, dump=False) + # print(str(reply)) return reply.payload_as_int() def read_fw_l_version(self) -> int: address = CommonRegisters.FIRMWARE_VERSION_L - reply: ReplyHarpMessage = self.send( - HarpMessage.ReadU8(address).frame, dump=False -) - #print(str(reply)) + reply: ReplyHarpMessage = self.send(HarpMessage.ReadU8(address).frame, dump=False) + # print(str(reply)) return reply.payload_as_int() @@ -180,10 +183,8 @@ def read_device_name(self) -> str: address = CommonRegisters.DEVICE_NAME # reply: Optional[bytes] = self.send(HarpMessage.ReadU8(address).frame, 13 + 24) - reply: ReplyHarpMessage = self.send( - HarpMessage.ReadU8(address).frame, dump=False - ) - #print(str(reply)) + reply: ReplyHarpMessage = self.send(HarpMessage.ReadU8(address).frame, dump=False) + # print(str(reply)) return reply.payload_as_string() @@ -209,13 +210,13 @@ def dump_registers(self) -> list: break return replies -# TODO: Not sure if we want to implement these. Delete if no. + # TODO: Not sure if we want to implement these. Delete if no. def set_mode(self, mode: DeviceMode) -> ReplyHarpMessage: """Change the device's OPMODE. Reply can be ignored.""" address = CommonRegisters.OPERATION_CTRL # Read register first. reg_value = self.send(HarpMessage.ReadU8(address).frame).payload_as_int() - reg_value &= ~0x03 # mask off old mode. + reg_value &= ~0x03 # mask off old mode. reg_value |= mode.value reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) return reply @@ -225,7 +226,7 @@ def enable_status_led(self): address = CommonRegisters.OPERATION_CTRL # Read register first. reg_value = self.send(HarpMessage.ReadU8(address).frame).payload_as_int() - reg_value |= (1 << 5) + reg_value |= 1 << 5 reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) def disable_status_led(self): @@ -241,7 +242,7 @@ def enable_alive_en(self): address = CommonRegisters.OPERATION_CTRL # Read register first. reg_value = self.send(HarpMessage.ReadU8(address).frame).payload_as_int() - reg_value |= (1 << 7) + reg_value |= 1 << 7 reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) def disable_alive_en(self): @@ -249,18 +250,22 @@ def disable_alive_en(self): address = CommonRegisters.OPERATION_CTRL # Read register first. reg_value = self.send(HarpMessage.ReadU8(address).frame).payload[0] - reg_value &= ((1<< 7) ^ 0xFF) # bitwise ~ operator substitute for Python ints. + reg_value &= (1 << 7) ^ 0xFF # bitwise ~ operator substitute for Python ints. reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) + def reset_device(self): + address = CommonRegisters.RESET_DEV + # reset_value = 0xFF & (1< ReplyHarpMessage: """Send a harp message; return the device's reply.""" - #print(f"Sending: {repr(message_bytes)}") + # print(f"Sending: {repr(message_bytes)}") self._ser.write(message_bytes) # TODO: handle case where read is None - # FIXME: waiting for a message reply like this - # breaks if events are also being broadcasted (i.e: in ActiveMode). reply: ReplyHarpMessage = self._read() if dump and self._dump_file_path is not None: @@ -268,40 +273,60 @@ def send(self, message_bytes: bytearray, dump: bool = True) -> ReplyHarpMessage: return reply + # def _read(self) -> Union[ReplyHarpMessage, None]: + # """(Blocking) Read an incoming serial message.""" + # # block for up to TIMEOUT until we get at least one byte. + # read_start = perf_counter() + # while True: + # if self._ser.inWaiting(): + # break + # if perf_counter() - read_start >= self.__class__.TIMEOUT_S: + # break + # try: + # message_type = self._ser.read(1)[0] # byte array with only one byte + # message_length = self._ser.read(1)[0] + # message_content = self._ser.read(message_length) + # self.log.debug(f"reply (type): {message_type}") + # self.log.debug(f"reply (length): {message_length}") + # self.log.debug(f"reply (payload): {message_content}") + # # print(f"Read back:") + # # print(f" type: {MessageType(message_type).name}") + # # print(f" length : {repr(message_length)}") + # # print(f" payload: {list(message_content)}") + + # frame = bytearray() + # frame.append(message_type) + # frame.append(message_length) + # frame += message_content + # msg = HarpMessage.parse(frame) + + # return msg + # except IndexError: + # return None def _read(self) -> Union[ReplyHarpMessage, None]: """(Blocking) Read an incoming serial message.""" - # block for up to TIMEOUT until we get at least one byte. - read_start = perf_counter() - while True: - if self._ser.inWaiting(): - break - if perf_counter() - read_start >= self.__class__.TIMEOUT_S: - break try: - message_type = self._ser.read(1)[0] # byte array with only one byte - message_length = self._ser.read(1)[0] - message_content = self._ser.read(message_length) - self.log.debug(f"reply (type): {message_type}") - self.log.debug(f"reply (length): {message_length}") - self.log.debug(f"reply (payload): {message_content}") - #print(f"Read back:") - #print(f" type: {MessageType(message_type).name}") - #print(f" length : {repr(message_length)}") - #print(f" payload: {list(message_content)}") - - frame = bytearray() - frame.append(message_type) - frame.append(message_length) - frame += message_content - msg = HarpMessage.parse(frame) - - return msg - except IndexError: + return self._ser.msg_q.get(block=True, timeout=self.read_timeout_s) + except queue.Empty: return None - def _dump_reply(self, reply: bytes): assert self._dump_file_path is not None with self._dump_file_path.open(mode="ab") as f: f.write(reply) + + # def read_register(self, register_name: str): + # register: Register = CommonRegisters[register_name] + # ReadHarpMessage(register.type, register.address) + + # def write_register(self, register_name: str, value): + + def get_events(self) -> list[ReplyHarpMessage]: + msgs = [] + while True: + try: + msgs.append(self._ser.event_q.get(timeout=False)) + except queue.Empty: + break + return msgs diff --git a/pyharp/harp_serial.py b/pyharp/harp_serial.py new file mode 100644 index 0000000..85a0bb8 --- /dev/null +++ b/pyharp/harp_serial.py @@ -0,0 +1,84 @@ +from typing import Union +from functools import partial +import logging +import queue +import threading +import serial +import serial.threaded + +from pyharp.messages import HarpMessage, MessageType + + +class HarpSerialProtocol(serial.threaded.Protocol): + _read_q: queue.Queue + + def __init__(self, _read_q: queue.Queue, *args, **kwargs): + self._read_q = _read_q + super().__init__(*args, **kwargs) + + def connection_made(self, transport: serial.threaded.ReaderThread) -> None: + print(f"Connected to {transport.serial.port}") + return super().connection_made(transport) + + def data_received(self, data: bytes) -> None: + for byte in data: + self._read_q.put(byte) + return super().data_received(data) + + def connection_lost(self, exc: Union[BaseException, None]) -> None: + print(f"Lost connection!") + return super().connection_lost(exc) + + +class HarpSerial: + + msg_q: queue.Queue + event_q: queue.Queue + + def __init__(self, serial_port: str, **kwargs): + self._ser = serial.Serial(serial_port, **kwargs) + + self.log = logging.getLogger(f"{__name__}.{self.__class__.__name__}") + + self._read_q = queue.Queue() + self.msg_q = queue.Queue() + self.event_q = queue.Queue() + + self._reader = serial.threaded.ReaderThread( + self._ser, + partial(HarpSerialProtocol, self._read_q), + ) + self._reader.start() + transport, protocol = self._reader.connect() + + self._parse_thread = threading.Thread( + target=self.parse_harp_msgs_threaded, + daemon=True, + ) + self._parse_thread.start() + + def close(self): + self._reader.close() + + def write(self, data): + self._reader.write(data) + + def parse_harp_msgs_threaded(self): + while True: + message_type = self._read_q.get(1) # byte array with only one byte + message_length = self._read_q.get(1) + message_content = bytes([self._read_q.get() for _ in range(message_length)]) + self.log.debug(f"reply (type): {message_type}") + self.log.debug(f"reply (length): {message_length}") + self.log.debug(f"reply (payload): {message_content}") + + frame = bytearray() + frame.append(message_type) + frame.append(message_length) + frame += message_content + msg = HarpMessage.parse(frame) + + if msg.message_type == MessageType.EVENT: + self.event_q.put(msg) + else: + self.msg_q.put(msg) diff --git a/pyharp/messages.py b/pyharp/messages.py index e86747a..2a91a25 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -1,7 +1,9 @@ -from __future__ import annotations # for type hints (PEP 563) +from __future__ import annotations # for type hints (PEP 563) from enum import Enum + # from abc import ABC, abstractmethod -from typing import Union, Tuple, Optional, List +from typing import Union, Tuple, Optional, List, Any +from dataclasses import dataclass import struct @@ -56,6 +58,16 @@ class CommonRegisters: DEVICE_NAME = 0x0C +@dataclass +class Register: + name: str + address: int + type: PayloadType + access: Optional[str] = None + description: Optional[str] = None + reset_value: Optional[Any] = None + + class HarpMessage: """ https://github.com/harp-tech/protocol/blob/master/Binary%20Protocol%201.0%201.1%2020180223.pdf @@ -167,9 +179,9 @@ def parse(frame: bytearray) -> ReplyHarpMessage: # A Response Message from a harp device. class ReplyHarpMessage(HarpMessage): - def __init__( - self, frame: bytearray, + self, + frame: bytearray, ): """ @@ -179,57 +191,66 @@ def __init__( self._frame = frame # retrieve all content from 11 (where payload starts) until the checksum (not inclusive) self._raw_payload = frame[11:-1] - self._payload = self._parse_payload(self._raw_payload) # payload formatted as list[payload type] + self._payload = self._parse_payload( + self._raw_payload + ) # payload formatted as list[payload type] # Assign timestamp after _payload since @properties all rely on self._payload. - self._timestamp = int.from_bytes(frame[5:9], byteorder="little", signed=False) + \ - int.from_bytes(frame[9:11], byteorder="little", signed=False)*32e-6 + self._timestamp = ( + int.from_bytes(frame[5:9], byteorder="little", signed=False) + + int.from_bytes(frame[9:11], byteorder="little", signed=False) * 32e-6 + ) # Timestamp is junk if it's not present. if not (self.payload_type.value & PayloadType.hasTimestamp.value): self._timestamp = None - def _parse_payload(self, raw_payload) -> list[int]: """return the payload as a list of ints after parsing it from the raw payload.""" is_signed = True if (self.payload_type.value & 0x80) else False is_float = True if (self.payload_type.value & 0x40) else False bytes_per_word = self.payload_type.value & 0x07 - payload_len = len(raw_payload) # payload length in bytes. + payload_len = len(raw_payload) # payload length in bytes. - word_chunks = [raw_payload[i:i+bytes_per_word] for i in range(0, payload_len, bytes_per_word)] + word_chunks = [ + raw_payload[i : i + bytes_per_word] + for i in range(0, payload_len, bytes_per_word) + ] if not is_float: - return [int.from_bytes(chunk, byteorder="little", signed=is_signed) for chunk in word_chunks] - else: # handle float case. - return [struct.unpack(' Union[int, list[int]]: @@ -254,7 +275,6 @@ def payload_as_float(self) -> float: class ReadHarpMessage(HarpMessage): MESSAGE_TYPE: int = MessageType.READ - def __init__(self, payload_type: PayloadType, address: int): self._frame = bytearray() @@ -287,6 +307,7 @@ class ReadS16HarpMessage(ReadHarpMessage): def __init__(self, address: int): super().__init__(PayloadType.S16, address) + class ReadU32HarpMessage(ReadHarpMessage): def __init__(self, address: int): super().__init__(PayloadType.U32, address) @@ -359,7 +380,10 @@ def payload(self) -> int: class WriteU16HarpMessage(WriteHarpMessage): def __init__(self, address: int, value: int): super().__init__( - PayloadType.U16, value.to_bytes(2, byteorder="little", signed=False), address, offset=1 + PayloadType.U16, + value.to_bytes(2, byteorder="little", signed=False), + address, + offset=1, ) @property @@ -385,20 +409,25 @@ class WriteFloatHarpMessage(WriteHarpMessage): def __init__(self, address: int, value: float): super().__init__( PayloadType.Float, - struct.pack(' float: - return struct.unpack(' int | List[int]: diff --git a/pyproject.toml b/pyproject.toml index ca8d302..9d3b6a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,7 @@ description = "Library for data acquisition and control of devices implementing authors = ["filcarv "] license = "MIT" readme = 'README.md' +python-versions = '>3.10' [tool.poetry.dependencies] python = "^3.4" diff --git a/tests/test_device.py b/tests/test_device.py index da16859..196a07c 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -1,6 +1,7 @@ import serial - +import time from typing import Optional + from pyharp.messages import HarpMessage, ReplyHarpMessage from pyharp.device import Device @@ -47,7 +48,7 @@ def test_U8() -> None: # write 65 on register 38 write_message = HarpMessage.WriteU8(register, write_value) - reply : ReplyHarpMessage = device.send(write_message.frame) + reply: ReplyHarpMessage = device.send(write_message.frame) assert reply is not None # read register 38 @@ -84,3 +85,25 @@ def test_U8() -> None: # assert not ser.is_open # # # assert data[0] == '\t' + + +def test_device_events(device: Device) -> None: + + event_q = device._ser.event_q + + while True: + print(device._ser.event_q.qsize()) + if not event_q.empty(): + try: + msg: ReplyHarpMessage = event_q.get() + print(msg) + except Exception: + pass + time.sleep(0.3) + + +if __name__ == "__main__": + # open serial connection and load info + device = Device("COM4", "dump.txt") + # assert device._dump_file_path.exists() + test_device_events(device) From 2381e8ff1d0050cb7cd8636f9b12605b661623b1 Mon Sep 17 00:00:00 2001 From: Patrick Latimer <110747402+patricklatimer@users.noreply.github.com> Date: Wed, 12 Feb 2025 14:56:44 -0800 Subject: [PATCH 027/267] clean up formatting changes --- pyharp/device.py | 103 ++++++++++++++++++--------------------------- pyharp/messages.py | 95 +++++++++++++++-------------------------- 2 files changed, 74 insertions(+), 124 deletions(-) diff --git a/pyharp/device.py b/pyharp/device.py index 4496859..0a754cb 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -31,7 +31,6 @@ class Device: https://github.com/harp-tech/protocol/blob/master/Device%201.1%201.0%2020220402.pdf """ - # _ser: serial.Serial _ser: HarpSerial _dump_file_path: Path @@ -91,7 +90,6 @@ def read(self): def connect(self) -> None: self._ser = HarpSerial( - # self._ser = serial.Serial( self._serial_port, # "/dev/tty.usbserial-A106C8O9" baudrate=1000000, timeout=self.__class__.TIMEOUT_S, @@ -110,7 +108,7 @@ def read_who_am_i(self) -> int: reply: ReplyHarpMessage = self.send( HarpMessage.ReadU16(address).frame, dump=False ) - # print(str(reply)) + #print(str(reply)) return reply.payload_as_int() def read_who_am_i_device(self) -> str: @@ -119,63 +117,77 @@ def read_who_am_i_device(self) -> str: reply: ReplyHarpMessage = self.send( HarpMessage.ReadU16(address).frame, dump=False ) - # print(str(reply)) + #print(str(reply)) return device_names.get(reply.payload_as_int()) def read_hw_version_h(self) -> int: address = CommonRegisters.HW_VERSION_H - reply: ReplyHarpMessage = self.send(HarpMessage.ReadU8(address).frame, dump=False) - # print(str(reply)) + reply: ReplyHarpMessage = self.send( + HarpMessage.ReadU8(address).frame, dump=False + ) + #print(str(reply)) return reply.payload_as_int() def read_hw_version_l(self) -> int: address = CommonRegisters.HW_VERSION_L - reply: ReplyHarpMessage = self.send(HarpMessage.ReadU8(address).frame, dump=False) - # print(str(reply)) + reply: ReplyHarpMessage = self.send( + HarpMessage.ReadU8(address).frame, dump=False + ) + #print(str(reply)) return reply.payload_as_int() def read_assembly_version(self) -> int: address = CommonRegisters.ASSEMBLY_VERSION - reply: ReplyHarpMessage = self.send(HarpMessage.ReadU8(address).frame, dump=False) - # print(str(reply)) + reply: ReplyHarpMessage = self.send( + HarpMessage.ReadU8(address).frame, dump=False + ) + #print(str(reply)) return reply.payload_as_int() def read_harp_h_version(self) -> int: address = CommonRegisters.HARP_VERSION_H - reply: ReplyHarpMessage = self.send(HarpMessage.ReadU8(address).frame, dump=False) - # print(str(reply)) + reply: ReplyHarpMessage = self.send( + HarpMessage.ReadU8(address).frame, dump=False + ) + #print(str(reply)) return reply.payload_as_int() def read_harp_l_version(self) -> int: address = CommonRegisters.HARP_VERSION_L - reply: ReplyHarpMessage = self.send(HarpMessage.ReadU8(address).frame, dump=False) - # print(str(reply)) + reply: ReplyHarpMessage = self.send( + HarpMessage.ReadU8(address).frame, dump=False + ) + #print(str(reply)) return reply.payload_as_int() def read_fw_h_version(self) -> int: address = CommonRegisters.FIRMWARE_VERSION_H - reply: ReplyHarpMessage = self.send(HarpMessage.ReadU8(address).frame, dump=False) - # print(str(reply)) + reply: ReplyHarpMessage = self.send( + HarpMessage.ReadU8(address).frame, dump=False + ) + #print(str(reply)) return reply.payload_as_int() def read_fw_l_version(self) -> int: address = CommonRegisters.FIRMWARE_VERSION_L - reply: ReplyHarpMessage = self.send(HarpMessage.ReadU8(address).frame, dump=False) - # print(str(reply)) + reply: ReplyHarpMessage = self.send( + HarpMessage.ReadU8(address).frame, dump=False +) + #print(str(reply)) return reply.payload_as_int() @@ -183,8 +195,10 @@ def read_device_name(self) -> str: address = CommonRegisters.DEVICE_NAME # reply: Optional[bytes] = self.send(HarpMessage.ReadU8(address).frame, 13 + 24) - reply: ReplyHarpMessage = self.send(HarpMessage.ReadU8(address).frame, dump=False) - # print(str(reply)) + reply: ReplyHarpMessage = self.send( + HarpMessage.ReadU8(address).frame, dump=False + ) + #print(str(reply)) return reply.payload_as_string() @@ -210,13 +224,13 @@ def dump_registers(self) -> list: break return replies - # TODO: Not sure if we want to implement these. Delete if no. +# TODO: Not sure if we want to implement these. Delete if no. def set_mode(self, mode: DeviceMode) -> ReplyHarpMessage: """Change the device's OPMODE. Reply can be ignored.""" address = CommonRegisters.OPERATION_CTRL # Read register first. reg_value = self.send(HarpMessage.ReadU8(address).frame).payload_as_int() - reg_value &= ~0x03 # mask off old mode. + reg_value &= ~0x03 # mask off old mode. reg_value |= mode.value reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) return reply @@ -226,7 +240,7 @@ def enable_status_led(self): address = CommonRegisters.OPERATION_CTRL # Read register first. reg_value = self.send(HarpMessage.ReadU8(address).frame).payload_as_int() - reg_value |= 1 << 5 + reg_value |= (1 << 5) reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) def disable_status_led(self): @@ -242,7 +256,7 @@ def enable_alive_en(self): address = CommonRegisters.OPERATION_CTRL # Read register first. reg_value = self.send(HarpMessage.ReadU8(address).frame).payload_as_int() - reg_value |= 1 << 7 + reg_value |= (1 << 7) reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) def disable_alive_en(self): @@ -250,7 +264,7 @@ def disable_alive_en(self): address = CommonRegisters.OPERATION_CTRL # Read register first. reg_value = self.send(HarpMessage.ReadU8(address).frame).payload[0] - reg_value &= (1 << 7) ^ 0xFF # bitwise ~ operator substitute for Python ints. + reg_value &= ((1<< 7) ^ 0xFF) # bitwise ~ operator substitute for Python ints. reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) def reset_device(self): @@ -262,7 +276,7 @@ def reset_device(self): def send(self, message_bytes: bytearray, dump: bool = True) -> ReplyHarpMessage: """Send a harp message; return the device's reply.""" - # print(f"Sending: {repr(message_bytes)}") + #print(f"Sending: {repr(message_bytes)}") self._ser.write(message_bytes) # TODO: handle case where read is None @@ -273,36 +287,6 @@ def send(self, message_bytes: bytearray, dump: bool = True) -> ReplyHarpMessage: return reply - # def _read(self) -> Union[ReplyHarpMessage, None]: - # """(Blocking) Read an incoming serial message.""" - # # block for up to TIMEOUT until we get at least one byte. - # read_start = perf_counter() - # while True: - # if self._ser.inWaiting(): - # break - # if perf_counter() - read_start >= self.__class__.TIMEOUT_S: - # break - # try: - # message_type = self._ser.read(1)[0] # byte array with only one byte - # message_length = self._ser.read(1)[0] - # message_content = self._ser.read(message_length) - # self.log.debug(f"reply (type): {message_type}") - # self.log.debug(f"reply (length): {message_length}") - # self.log.debug(f"reply (payload): {message_content}") - # # print(f"Read back:") - # # print(f" type: {MessageType(message_type).name}") - # # print(f" length : {repr(message_length)}") - # # print(f" payload: {list(message_content)}") - - # frame = bytearray() - # frame.append(message_type) - # frame.append(message_length) - # frame += message_content - # msg = HarpMessage.parse(frame) - - # return msg - # except IndexError: - # return None def _read(self) -> Union[ReplyHarpMessage, None]: """(Blocking) Read an incoming serial message.""" @@ -310,18 +294,11 @@ def _read(self) -> Union[ReplyHarpMessage, None]: return self._ser.msg_q.get(block=True, timeout=self.read_timeout_s) except queue.Empty: return None - def _dump_reply(self, reply: bytes): assert self._dump_file_path is not None with self._dump_file_path.open(mode="ab") as f: f.write(reply) - # def read_register(self, register_name: str): - # register: Register = CommonRegisters[register_name] - # ReadHarpMessage(register.type, register.address) - - # def write_register(self, register_name: str, value): - def get_events(self) -> list[ReplyHarpMessage]: msgs = [] while True: diff --git a/pyharp/messages.py b/pyharp/messages.py index 2a91a25..e86747a 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -1,9 +1,7 @@ -from __future__ import annotations # for type hints (PEP 563) +from __future__ import annotations # for type hints (PEP 563) from enum import Enum - # from abc import ABC, abstractmethod -from typing import Union, Tuple, Optional, List, Any -from dataclasses import dataclass +from typing import Union, Tuple, Optional, List import struct @@ -58,16 +56,6 @@ class CommonRegisters: DEVICE_NAME = 0x0C -@dataclass -class Register: - name: str - address: int - type: PayloadType - access: Optional[str] = None - description: Optional[str] = None - reset_value: Optional[Any] = None - - class HarpMessage: """ https://github.com/harp-tech/protocol/blob/master/Binary%20Protocol%201.0%201.1%2020180223.pdf @@ -179,9 +167,9 @@ def parse(frame: bytearray) -> ReplyHarpMessage: # A Response Message from a harp device. class ReplyHarpMessage(HarpMessage): + def __init__( - self, - frame: bytearray, + self, frame: bytearray, ): """ @@ -191,66 +179,57 @@ def __init__( self._frame = frame # retrieve all content from 11 (where payload starts) until the checksum (not inclusive) self._raw_payload = frame[11:-1] - self._payload = self._parse_payload( - self._raw_payload - ) # payload formatted as list[payload type] + self._payload = self._parse_payload(self._raw_payload) # payload formatted as list[payload type] # Assign timestamp after _payload since @properties all rely on self._payload. - self._timestamp = ( - int.from_bytes(frame[5:9], byteorder="little", signed=False) - + int.from_bytes(frame[9:11], byteorder="little", signed=False) * 32e-6 - ) + self._timestamp = int.from_bytes(frame[5:9], byteorder="little", signed=False) + \ + int.from_bytes(frame[9:11], byteorder="little", signed=False)*32e-6 # Timestamp is junk if it's not present. if not (self.payload_type.value & PayloadType.hasTimestamp.value): self._timestamp = None + def _parse_payload(self, raw_payload) -> list[int]: """return the payload as a list of ints after parsing it from the raw payload.""" is_signed = True if (self.payload_type.value & 0x80) else False is_float = True if (self.payload_type.value & 0x40) else False bytes_per_word = self.payload_type.value & 0x07 - payload_len = len(raw_payload) # payload length in bytes. + payload_len = len(raw_payload) # payload length in bytes. - word_chunks = [ - raw_payload[i : i + bytes_per_word] - for i in range(0, payload_len, bytes_per_word) - ] + word_chunks = [raw_payload[i:i+bytes_per_word] for i in range(0, payload_len, bytes_per_word)] if not is_float: - return [ - int.from_bytes(chunk, byteorder="little", signed=is_signed) - for chunk in word_chunks - ] - else: # handle float case. - return [struct.unpack(" Union[int, list[int]]: @@ -275,6 +254,7 @@ def payload_as_float(self) -> float: class ReadHarpMessage(HarpMessage): MESSAGE_TYPE: int = MessageType.READ + def __init__(self, payload_type: PayloadType, address: int): self._frame = bytearray() @@ -307,7 +287,6 @@ class ReadS16HarpMessage(ReadHarpMessage): def __init__(self, address: int): super().__init__(PayloadType.S16, address) - class ReadU32HarpMessage(ReadHarpMessage): def __init__(self, address: int): super().__init__(PayloadType.U32, address) @@ -380,10 +359,7 @@ def payload(self) -> int: class WriteU16HarpMessage(WriteHarpMessage): def __init__(self, address: int, value: int): super().__init__( - PayloadType.U16, - value.to_bytes(2, byteorder="little", signed=False), - address, - offset=1, + PayloadType.U16, value.to_bytes(2, byteorder="little", signed=False), address, offset=1 ) @property @@ -409,25 +385,20 @@ class WriteFloatHarpMessage(WriteHarpMessage): def __init__(self, address: int, value: float): super().__init__( PayloadType.Float, - struct.pack( - " float: - return struct.unpack(" int | List[int]: From 9e305456fd539d4e317edd5af0fab023f1ccae50 Mon Sep 17 00:00:00 2001 From: Patrick Latimer Date: Wed, 12 Feb 2025 15:53:35 -0800 Subject: [PATCH 028/267] add event_count method and update test --- pyharp/device.py | 15 +++++++-------- pyharp/harp_serial.py | 4 ++-- tests/test_device.py | 20 +++----------------- 3 files changed, 12 insertions(+), 27 deletions(-) diff --git a/pyharp/device.py b/pyharp/device.py index 0a754cb..0f4368e 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -6,13 +6,7 @@ from pathlib import Path from pyharp.harp_serial import HarpSerial -from pyharp.messages import ( - HarpMessage, - ReadHarpMessage, - ReplyHarpMessage, - # ResetDevOffsets, - Register, -) +from pyharp.messages import HarpMessage, ReplyHarpMessage from pyharp.messages import CommonRegisters, MessageType from pyharp.device_names import device_names from enum import Enum @@ -287,19 +281,20 @@ def send(self, message_bytes: bytearray, dump: bool = True) -> ReplyHarpMessage: return reply - def _read(self) -> Union[ReplyHarpMessage, None]: """(Blocking) Read an incoming serial message.""" try: return self._ser.msg_q.get(block=True, timeout=self.read_timeout_s) except queue.Empty: return None + def _dump_reply(self, reply: bytes): assert self._dump_file_path is not None with self._dump_file_path.open(mode="ab") as f: f.write(reply) def get_events(self) -> list[ReplyHarpMessage]: + """Get all events from the event queue.""" msgs = [] while True: try: @@ -307,3 +302,7 @@ def get_events(self) -> list[ReplyHarpMessage]: except queue.Empty: break return msgs + + def event_count(self) -> int: + """Get the number of events in the event queue.""" + return self._ser.event_q.qsize() \ No newline at end of file diff --git a/pyharp/harp_serial.py b/pyharp/harp_serial.py index 85a0bb8..3b9d4a2 100644 --- a/pyharp/harp_serial.py +++ b/pyharp/harp_serial.py @@ -17,7 +17,7 @@ def __init__(self, _read_q: queue.Queue, *args, **kwargs): super().__init__(*args, **kwargs) def connection_made(self, transport: serial.threaded.ReaderThread) -> None: - print(f"Connected to {transport.serial.port}") + # print(f"Connected to {transport.serial.port}") return super().connection_made(transport) def data_received(self, data: bytes) -> None: @@ -26,7 +26,7 @@ def data_received(self, data: bytes) -> None: return super().data_received(data) def connection_lost(self, exc: Union[BaseException, None]) -> None: - print(f"Lost connection!") + # print(f"Lost connection!") return super().connection_lost(exc) diff --git a/tests/test_device.py b/tests/test_device.py index 196a07c..9501663 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -1,7 +1,6 @@ import serial import time from typing import Optional - from pyharp.messages import HarpMessage, ReplyHarpMessage from pyharp.device import Device @@ -89,21 +88,8 @@ def test_U8() -> None: def test_device_events(device: Device) -> None: - event_q = device._ser.event_q - while True: - print(device._ser.event_q.qsize()) - if not event_q.empty(): - try: - msg: ReplyHarpMessage = event_q.get() - print(msg) - except Exception: - pass + print(device.event_count()) + for msg in device.get_events(): + print(msg) time.sleep(0.3) - - -if __name__ == "__main__": - # open serial connection and load info - device = Device("COM4", "dump.txt") - # assert device._dump_file_path.exists() - test_device_events(device) From 0d993bb8d522eafd976e1aca0cd85cc9eec1b734 Mon Sep 17 00:00:00 2001 From: Patrick Latimer Date: Wed, 12 Feb 2025 15:54:56 -0800 Subject: [PATCH 029/267] remove unused import --- pyharp/device.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyharp/device.py b/pyharp/device.py index 0f4368e..b1c8a3b 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -1,6 +1,5 @@ import serial import logging -import threading import queue from typing import Optional, Union from pathlib import Path From 583a2709d5dab14d09bc86f54b3439e6e7854695 Mon Sep 17 00:00:00 2001 From: Patrick Latimer Date: Wed, 12 Feb 2025 15:57:43 -0800 Subject: [PATCH 030/267] undo formatting change --- tests/test_device.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_device.py b/tests/test_device.py index 9501663..5a5eab9 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -47,7 +47,7 @@ def test_U8() -> None: # write 65 on register 38 write_message = HarpMessage.WriteU8(register, write_value) - reply: ReplyHarpMessage = device.send(write_message.frame) + reply : ReplyHarpMessage = device.send(write_message.frame) assert reply is not None # read register 38 @@ -87,7 +87,6 @@ def test_U8() -> None: def test_device_events(device: Device) -> None: - while True: print(device.event_count()) for msg in device.get_events(): From 556e51263c35b7d6d91e5d944ba82415fd580de1 Mon Sep 17 00:00:00 2001 From: Patrick Latimer Date: Wed, 12 Feb 2025 15:59:17 -0800 Subject: [PATCH 031/267] remove python-versions pin --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9d3b6a2..ca8d302 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,6 @@ description = "Library for data acquisition and control of devices implementing authors = ["filcarv "] license = "MIT" readme = 'README.md' -python-versions = '>3.10' [tool.poetry.dependencies] python = "^3.4" From efdefc5d9387b1662a7c6c8e03fba137a2408c2a Mon Sep 17 00:00:00 2001 From: Patrick Latimer Date: Wed, 12 Feb 2025 16:53:54 -0800 Subject: [PATCH 032/267] add future import for list annotations --- pyharp/device.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyharp/device.py b/pyharp/device.py index b1c8a3b..7bfdf76 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -3,6 +3,7 @@ import queue from typing import Optional, Union from pathlib import Path +from __future__ import annotations # enable subscriptable type hints for lists. from pyharp.harp_serial import HarpSerial from pyharp.messages import HarpMessage, ReplyHarpMessage From 0a15ab28fc5fd02c871646de37aa1b3b4a4727f4 Mon Sep 17 00:00:00 2001 From: Patrick Latimer Date: Wed, 12 Feb 2025 16:57:42 -0800 Subject: [PATCH 033/267] fix future import --- pyharp/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyharp/device.py b/pyharp/device.py index 7bfdf76..b4c6f6d 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -1,9 +1,9 @@ +from __future__ import annotations # enable subscriptable type hints for lists. import serial import logging import queue from typing import Optional, Union from pathlib import Path -from __future__ import annotations # enable subscriptable type hints for lists. from pyharp.harp_serial import HarpSerial from pyharp.messages import HarpMessage, ReplyHarpMessage From c68f87b542d8109bceafa7ad7f61813ae8da5fd9 Mon Sep 17 00:00:00 2001 From: Sonya Vasquez Date: Thu, 20 Feb 2025 16:09:49 -0800 Subject: [PATCH 034/267] update wait-for-events example --- examples/get_info.py | 5 +++-- examples/wait_for_events.py | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/get_info.py b/examples/get_info.py index d7b7a47..eec8cef 100755 --- a/examples/get_info.py +++ b/examples/get_info.py @@ -37,8 +37,9 @@ device_assembly = device.ASSEMBLY_VERSION # Get device's assembly version reg_dump = device.dump_registers() -for i in range(11): - print(reg_dump[i]) +for reg_reply in reg_dump: + print(reg_reply) + print() # Close connection device.disconnect() diff --git a/examples/wait_for_events.py b/examples/wait_for_events.py index b846d37..99fbe15 100755 --- a/examples/wait_for_events.py +++ b/examples/wait_for_events.py @@ -24,7 +24,8 @@ #device.disable_all_events() #device.enable_events(Events.port_digital_inputs) while True: - event_response = device._read() # read any incoming events. - if event_response is not None:# and event_response.address != 44: + if not device.event_count(): + pass + for msg in device.get_events(): + print(msg) print() - print(event_response) From de151be77eab4a279e5b0e408e8c57fc4e6ff843 Mon Sep 17 00:00:00 2001 From: Sonya Vasquez Date: Fri, 21 Feb 2025 10:50:47 -0800 Subject: [PATCH 035/267] remove extraneous call --- examples/wait_for_events.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/wait_for_events.py b/examples/wait_for_events.py index 99fbe15..584836e 100755 --- a/examples/wait_for_events.py +++ b/examples/wait_for_events.py @@ -24,8 +24,6 @@ #device.disable_all_events() #device.enable_events(Events.port_digital_inputs) while True: - if not device.event_count(): - pass for msg in device.get_events(): print(msg) print() From e86eb296f31f83f652922ef6e47d3d60f7d5e1bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 19 Mar 2025 11:43:51 +0000 Subject: [PATCH 036/267] Convert project to uv --- pyproject.toml | 28 ++++++------- uv.lock | 111 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 15 deletions(-) create mode 100644 uv.lock diff --git a/pyproject.toml b/pyproject.toml index ca8d302..325a289 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,23 +1,21 @@ -[tool.poetry] +[project] name = "pyharp" version = "0.1.0" description = "Library for data acquisition and control of devices implementing the Harp protocol." -authors = ["filcarv "] +authors = [{ name= "Filipe Carvalho", email="filipe@open-ephys.org"}] license = "MIT" readme = 'README.md' +requires-python = ">=3.11" +dependencies = [ + "pyserial>=3.5", +] -[tool.poetry.dependencies] -python = "^3.4" -pyserial = "^3.4" - -[tool.poetry.dev-dependencies] -pytest = "^5.2" -mypy = "^0.782" -black = "^19.10b0" +[dependency-groups] +dev = [ + "pytest>=8.3.5", + "ruff>=0.11.0", +] [build-system] -requires = ["poetry-core>=1.0.8"] -build-backend = "poetry.core.masonry.api" - -[tool.poetry.scripts] -pyharp = "pyharp.main:main" +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..9b482ec --- /dev/null +++ b/uv.lock @@ -0,0 +1,111 @@ +version = 1 +revision = 1 +requires-python = ">=3.11" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pyharp" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "pyserial" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [{ name = "pyserial", specifier = ">=3.5" }] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8.3.5" }, + { name = "ruff", specifier = ">=0.11.0" }, +] + +[[package]] +name = "pyserial" +version = "3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/7d/ae3f0a63f41e4d2f6cb66a5b57197850f919f59e558159a4dd3a818f5082/pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb", size = 159125 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0", size = 90585 }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, +] + +[[package]] +name = "ruff" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/77/2b/7ca27e854d92df5e681e6527dc0f9254c9dc06c8408317893cf96c851cdd/ruff-0.11.0.tar.gz", hash = "sha256:e55c620690a4a7ee6f1cccb256ec2157dc597d109400ae75bbf944fc9d6462e2", size = 3799407 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/40/3d0340a9e5edc77d37852c0cd98c5985a5a8081fc3befaeb2ae90aaafd2b/ruff-0.11.0-py3-none-linux_armv6l.whl", hash = "sha256:dc67e32bc3b29557513eb7eeabb23efdb25753684b913bebb8a0c62495095acb", size = 10098158 }, + { url = "https://files.pythonhosted.org/packages/ec/a9/d8f5abb3b87b973b007649ac7bf63665a05b2ae2b2af39217b09f52abbbf/ruff-0.11.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38c23fd9bdec4eb437b4c1e3595905a0a8edfccd63a790f818b28c78fe345639", size = 10879071 }, + { url = "https://files.pythonhosted.org/packages/ab/62/aaa198614c6211677913ec480415c5e6509586d7b796356cec73a2f8a3e6/ruff-0.11.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7c8661b0be91a38bd56db593e9331beaf9064a79028adee2d5f392674bbc5e88", size = 10247944 }, + { url = "https://files.pythonhosted.org/packages/9f/52/59e0a9f2cf1ce5e6cbe336b6dd0144725c8ea3b97cac60688f4e7880bf13/ruff-0.11.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6c0e8d3d2db7e9f6efd884f44b8dc542d5b6b590fc4bb334fdbc624d93a29a2", size = 10421725 }, + { url = "https://files.pythonhosted.org/packages/a6/c3/dcd71acc6dff72ce66d13f4be5bca1dbed4db678dff2f0f6f307b04e5c02/ruff-0.11.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c3156d3f4b42e57247275a0a7e15a851c165a4fc89c5e8fa30ea6da4f7407b8", size = 9954435 }, + { url = "https://files.pythonhosted.org/packages/a6/9a/342d336c7c52dbd136dee97d4c7797e66c3f92df804f8f3b30da59b92e9c/ruff-0.11.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:490b1e147c1260545f6d041c4092483e3f6d8eba81dc2875eaebcf9140b53905", size = 11492664 }, + { url = "https://files.pythonhosted.org/packages/84/35/6e7defd2d7ca95cc385ac1bd9f7f2e4a61b9cc35d60a263aebc8e590c462/ruff-0.11.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1bc09a7419e09662983b1312f6fa5dab829d6ab5d11f18c3760be7ca521c9329", size = 12207856 }, + { url = "https://files.pythonhosted.org/packages/22/78/da669c8731bacf40001c880ada6d31bcfb81f89cc996230c3b80d319993e/ruff-0.11.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcfa478daf61ac8002214eb2ca5f3e9365048506a9d52b11bea3ecea822bb844", size = 11645156 }, + { url = "https://files.pythonhosted.org/packages/ee/47/e27d17d83530a208f4a9ab2e94f758574a04c51e492aa58f91a3ed7cbbcb/ruff-0.11.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6fbb2aed66fe742a6a3a0075ed467a459b7cedc5ae01008340075909d819df1e", size = 13884167 }, + { url = "https://files.pythonhosted.org/packages/9f/5e/42ffbb0a5d4b07bbc642b7d58357b4e19a0f4774275ca6ca7d1f7b5452cd/ruff-0.11.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92c0c1ff014351c0b0cdfdb1e35fa83b780f1e065667167bb9502d47ca41e6db", size = 11348311 }, + { url = "https://files.pythonhosted.org/packages/c8/51/dc3ce0c5ce1a586727a3444a32f98b83ba99599bb1ebca29d9302886e87f/ruff-0.11.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e4fd5ff5de5f83e0458a138e8a869c7c5e907541aec32b707f57cf9a5e124445", size = 10305039 }, + { url = "https://files.pythonhosted.org/packages/60/e0/475f0c2f26280f46f2d6d1df1ba96b3399e0234cf368cc4c88e6ad10dcd9/ruff-0.11.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:96bc89a5c5fd21a04939773f9e0e276308be0935de06845110f43fd5c2e4ead7", size = 9937939 }, + { url = "https://files.pythonhosted.org/packages/e2/d3/3e61b7fd3e9cdd1e5b8c7ac188bec12975c824e51c5cd3d64caf81b0331e/ruff-0.11.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a9352b9d767889ec5df1483f94870564e8102d4d7e99da52ebf564b882cdc2c7", size = 10923259 }, + { url = "https://files.pythonhosted.org/packages/30/32/cd74149ebb40b62ddd14bd2d1842149aeb7f74191fb0f49bd45c76909ff2/ruff-0.11.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:049a191969a10897fe052ef9cc7491b3ef6de79acd7790af7d7897b7a9bfbcb6", size = 11406212 }, + { url = "https://files.pythonhosted.org/packages/00/ef/033022a6b104be32e899b00de704d7c6d1723a54d4c9e09d147368f14b62/ruff-0.11.0-py3-none-win32.whl", hash = "sha256:3191e9116b6b5bbe187447656f0c8526f0d36b6fd89ad78ccaad6bdc2fad7df2", size = 10310905 }, + { url = "https://files.pythonhosted.org/packages/ed/8a/163f2e78c37757d035bd56cd60c8d96312904ca4a6deeab8442d7b3cbf89/ruff-0.11.0-py3-none-win_amd64.whl", hash = "sha256:c58bfa00e740ca0a6c43d41fb004cd22d165302f360aaa56f7126d544db31a21", size = 11411730 }, + { url = "https://files.pythonhosted.org/packages/4e/f7/096f6efabe69b49d7ca61052fc70289c05d8d35735c137ef5ba5ef423662/ruff-0.11.0-py3-none-win_arm64.whl", hash = "sha256:868364fc23f5aa122b00c6f794211e85f7e78f5dffdf7c590ab90b8c4e69b657", size = 10538956 }, +] From 0163e17c920a254f7407c0dd7a9ac1a39f486654 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 19 Mar 2025 13:31:58 +0000 Subject: [PATCH 037/267] Define docstyle convention --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 325a289..ee807f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,3 +19,6 @@ dev = [ [build-system] requires = ["hatchling"] build-backend = "hatchling.build" + +[tool.ruff.lint.pydocstyle] +convention = "numpy" \ No newline at end of file From 15ac05e663ef19d6cd0f694c93215189f18efd9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Grilo?= Date: Wed, 19 Mar 2025 15:07:49 +0000 Subject: [PATCH 038/267] Update default vscode settings --- .vscode/extensions.json | 6 ++++++ .vscode/settings.json | 9 +++++++++ 2 files changed, 15 insertions(+) create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..4e265ea --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "ms-python.python", + "charliermarsh.ruff" + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..dec7265 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + "ruff.organizeImports": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + } +} \ No newline at end of file From efceffd3451fbfd62ed0f782009e5a7b41de6ffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Grilo?= Date: Wed, 19 Mar 2025 16:50:27 +0000 Subject: [PATCH 039/267] Add docs to the project --- docs/index.md | 4 + mkdocs.yml | 16 ++ pyproject.toml | 5 +- uv.lock | 490 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 514 insertions(+), 1 deletion(-) create mode 100644 docs/index.md create mode 100644 mkdocs.yml diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..c0f9b06 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,4 @@ +# pyharp + +!!! Warning + Work in Progress! \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..e4b6a6e --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,16 @@ +site_name: pyharp + +plugins: + - mkdocstrings: + handlers: + python: + options: + docstring_style: numpy + +markdown_extensions: + - admonition + - pymdownx.details + - pymdownx.superfences + +theme: + name: material diff --git a/pyproject.toml b/pyproject.toml index ee807f4..14f8aa5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,9 @@ dependencies = [ [dependency-groups] dev = [ + "mkdocs>=1.6.1", + "mkdocs-material>=9.6.9", + "mkdocstrings-python>=1.16.6", "pytest>=8.3.5", "ruff>=0.11.0", ] @@ -21,4 +24,4 @@ requires = ["hatchling"] build-backend = "hatchling.build" [tool.ruff.lint.pydocstyle] -convention = "numpy" \ No newline at end of file +convention = "numpy" diff --git a/uv.lock b/uv.lock index 9b482ec..984b6c6 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,97 @@ version = 1 revision = 1 requires-python = ">=3.11" +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537 }, +] + +[[package]] +name = "backrefs" +version = "5.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/46/caba1eb32fa5784428ab401a5487f73db4104590ecd939ed9daaf18b47e0/backrefs-5.8.tar.gz", hash = "sha256:2cab642a205ce966af3dd4b38ee36009b31fa9502a35fd61d59ccc116e40a6bd", size = 6773994 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/cb/d019ab87fe70e0fe3946196d50d6a4428623dc0c38a6669c8cae0320fbf3/backrefs-5.8-py310-none-any.whl", hash = "sha256:c67f6638a34a5b8730812f5101376f9d41dc38c43f1fdc35cb54700f6ed4465d", size = 380337 }, + { url = "https://files.pythonhosted.org/packages/a9/86/abd17f50ee21b2248075cb6924c6e7f9d23b4925ca64ec660e869c2633f1/backrefs-5.8-py311-none-any.whl", hash = "sha256:2e1c15e4af0e12e45c8701bd5da0902d326b2e200cafcd25e49d9f06d44bb61b", size = 392142 }, + { url = "https://files.pythonhosted.org/packages/b3/04/7b415bd75c8ab3268cc138c76fa648c19495fcc7d155508a0e62f3f82308/backrefs-5.8-py312-none-any.whl", hash = "sha256:bbef7169a33811080d67cdf1538c8289f76f0942ff971222a16034da88a73486", size = 398021 }, + { url = "https://files.pythonhosted.org/packages/04/b8/60dcfb90eb03a06e883a92abbc2ab95c71f0d8c9dd0af76ab1d5ce0b1402/backrefs-5.8-py313-none-any.whl", hash = "sha256:e3a63b073867dbefd0536425f43db618578528e3896fb77be7141328642a1585", size = 399915 }, + { url = "https://files.pythonhosted.org/packages/0c/37/fb6973edeb700f6e3d6ff222400602ab1830446c25c7b4676d8de93e65b8/backrefs-5.8-py39-none-any.whl", hash = "sha256:a66851e4533fb5b371aa0628e1fee1af05135616b86140c9d787a2ffdf4b8fdc", size = 380336 }, +] + +[[package]] +name = "certifi" +version = "2025.1.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 }, + { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 }, + { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 }, + { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 }, + { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 }, + { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 }, + { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 }, + { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 }, + { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 }, + { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 }, + { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 }, + { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 }, + { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 }, + { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, + { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, + { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, + { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, + { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, + { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, + { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, + { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, + { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, + { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, + { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, + { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, + { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, + { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, + { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, + { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, + { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, + { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, + { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, + { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, + { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, + { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, + { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, + { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, + { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, + { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -11,6 +102,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034 }, +] + +[[package]] +name = "griffe" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/ba/1ebe51a22c491a3fc94b44ef9c46a5b5472540e24a5c3f251cebbab7214b/griffe-1.6.1.tar.gz", hash = "sha256:ff0acf706b2680f8c721412623091c891e752b2c61b7037618f7b77d06732cf5", size = 393112 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/d3/a760d1062e44587230aa65573c70edaad4ee8a0e60e193a3172b304d24d8/griffe-1.6.1-py3-none-any.whl", hash = "sha256:b0131670db16834f82383bcf4f788778853c9bf4dc7a1a2b708bb0808ca56a98", size = 128615 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + [[package]] name = "iniconfig" version = "2.0.0" @@ -20,6 +144,198 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, ] +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, +] + +[[package]] +name = "markdown" +version = "3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354 }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451 }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/44/140469d87379c02f1e1870315f3143718036a983dd0416650827b8883192/mkdocs_autorefs-1.4.1.tar.gz", hash = "sha256:4b5b6235a4becb2b10425c2fa191737e415b37aa3418919db33e5d774c9db079", size = 4131355 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/29/1125f7b11db63e8e32bcfa0752a4eea30abff3ebd0796f808e14571ddaa2/mkdocs_autorefs-1.4.1-py3-none-any.whl", hash = "sha256:9793c5ac06a6ebbe52ec0f8439256e66187badf4b5334b5fde0b128ec134df4f", size = 5782047 }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521 }, +] + +[[package]] +name = "mkdocs-material" +version = "9.6.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/cb/6dd3b6a7925429c0229738098ee874dbf7fa02db55558adb2c5bf86077b2/mkdocs_material-9.6.9.tar.gz", hash = "sha256:a4872139715a1f27b2aa3f3dc31a9794b7bbf36333c0ba4607cf04786c94f89c", size = 3948083 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/7c/ea5a671b2ff5d0e3f3108a7f7d75b541d683e4969aaead2a8f3e59e0fc27/mkdocs_material-9.6.9-py3-none-any.whl", hash = "sha256:6e61b7fb623ce2aa4622056592b155a9eea56ff3487d0835075360be45a4c8d1", size = 8697935 }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728 }, +] + +[[package]] +name = "mkdocstrings" +version = "0.29.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "pymdown-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/4d/a9484dc5d926295bdf308f1f6c4f07fcc99735b970591edc414d401fcc91/mkdocstrings-0.29.0.tar.gz", hash = "sha256:3657be1384543ce0ee82112c3e521bbf48e41303aa0c229b9ffcccba057d922e", size = 1212185 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/47/eb876dfd84e48f31ff60897d161b309cf6a04ca270155b0662aae562b3fb/mkdocstrings-0.29.0-py3-none-any.whl", hash = "sha256:8ea98358d2006f60befa940fdebbbc88a26b37ecbcded10be726ba359284f73d", size = 1630824 }, +] + +[[package]] +name = "mkdocstrings-python" +version = "1.16.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffe" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/e7/0691e34e807a8f5c28f0988fcfeeb584f0b569ce433bf341944f14bdb3ff/mkdocstrings_python-1.16.6.tar.gz", hash = "sha256:cefe0f0e17ab4a4611f01b0a2af75e4298664e0ff54feb83c91a485bfed82dc9", size = 201565 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/42/ed682687ef5f248e104f82806d5d9893f6dd81d8cb4561692e190ba1a252/mkdocstrings_python-1.16.6-py3-none-any.whl", hash = "sha256:de877dd71f69878c973c4897a39683b7b6961bee7b058879095b69681488453f", size = 123207 }, +] + [[package]] name = "packaging" version = "24.2" @@ -29,6 +345,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, ] +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746 }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, +] + [[package]] name = "pluggy" version = "1.5.0" @@ -38,6 +381,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, ] +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, +] + [[package]] name = "pyharp" version = "0.1.0" @@ -48,6 +400,9 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "mkdocs" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings-python" }, { name = "pytest" }, { name = "ruff" }, ] @@ -57,10 +412,26 @@ requires-dist = [{ name = "pyserial", specifier = ">=3.5" }] [package.metadata.requires-dev] dev = [ + { name = "mkdocs", specifier = ">=1.6.1" }, + { name = "mkdocs-material", specifier = ">=9.6.9" }, + { name = "mkdocstrings-python", specifier = ">=1.16.6" }, { name = "pytest", specifier = ">=8.3.5" }, { name = "ruff", specifier = ">=0.11.0" }, ] +[[package]] +name = "pymdown-extensions" +version = "10.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/44/e6de2fdc880ad0ec7547ca2e087212be815efbc9a425a8d5ba9ede602cbb/pymdown_extensions-10.14.3.tar.gz", hash = "sha256:41e576ce3f5d650be59e900e4ceff231e0aed2a88cf30acaee41e02f063a061b", size = 846846 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/f5/b9e2a42aa8f9e34d52d66de87941ecd236570c7ed2e87775ed23bbe4e224/pymdown_extensions-10.14.3-py3-none-any.whl", hash = "sha256:05e0bee73d64b9c71a4ae17c72abc2f700e8bc8403755a00580b49a4e9f189e9", size = 264467 }, +] + [[package]] name = "pyserial" version = "3.5" @@ -85,6 +456,80 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/8e/da1c6c58f751b70f8ceb1eb25bc25d524e8f14fe16edcce3f4e3ba08629c/pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb", size = 5631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/66/bbb1dd374f5c870f59c5bb1db0e18cbe7fa739415a24cbd95b2d1f5ae0c4/pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069", size = 3911 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + [[package]] name = "ruff" version = "0.11.0" @@ -109,3 +554,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ed/8a/163f2e78c37757d035bd56cd60c8d96312904ca4a6deeab8442d7b3cbf89/ruff-0.11.0-py3-none-win_amd64.whl", hash = "sha256:c58bfa00e740ca0a6c43d41fb004cd22d165302f360aaa56f7126d544db31a21", size = 11411730 }, { url = "https://files.pythonhosted.org/packages/4e/f7/096f6efabe69b49d7ca61052fc70289c05d8d35735c137ef5ba5ef423662/ruff-0.11.0-py3-none-win_arm64.whl", hash = "sha256:868364fc23f5aa122b00c6f794211e85f7e78f5dffdf7c590ab90b8c4e69b657", size = 10538956 }, ] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "urllib3" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393 }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392 }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019 }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471 }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449 }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054 }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480 }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451 }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057 }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079 }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076 }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065 }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 }, +] From 627b1dbede50f0c2ed543cf357fabfe78b962d4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Grilo?= Date: Wed, 19 Mar 2025 16:50:52 +0000 Subject: [PATCH 040/267] Delete poetry.lock --- poetry.lock | 415 ---------------------------------------------------- 1 file changed, 415 deletions(-) delete mode 100644 poetry.lock diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 6e3d0a4..0000000 --- a/poetry.lock +++ /dev/null @@ -1,415 +0,0 @@ -[[package]] -category = "main" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -name = "appdirs" -optional = false -python-versions = "*" -version = "1.4.4" - -[[package]] -category = "dev" -description = "Atomic file writes." -marker = "sys_platform == \"win32\"" -name = "atomicwrites" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.4.0" - -[[package]] -category = "main" -description = "Classes Without Boilerplate" -name = "attrs" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "19.3.0" - -[package.extras] -azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] -dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] -docs = ["sphinx", "zope.interface"] -tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] - -[[package]] -category = "main" -description = "The uncompromising code formatter." -name = "black" -optional = false -python-versions = ">=3.6" -version = "19.10b0" - -[package.dependencies] -appdirs = "*" -attrs = ">=18.1.0" -click = ">=6.5" -pathspec = ">=0.6,<1" -regex = "*" -toml = ">=0.9.4" -typed-ast = ">=1.4.0" - -[package.extras] -d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] - -[[package]] -category = "main" -description = "Composable command line interface toolkit" -name = "click" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "7.1.2" - -[[package]] -category = "dev" -description = "Cross-platform colored terminal text." -marker = "sys_platform == \"win32\"" -name = "colorama" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.4.3" - -[[package]] -category = "dev" -description = "Read metadata from Python packages" -marker = "python_version < \"3.8\"" -name = "importlib-metadata" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "1.7.0" - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -docs = ["sphinx", "rst.linker"] -testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] - -[[package]] -category = "dev" -description = "More routines for operating on iterables, beyond itertools" -name = "more-itertools" -optional = false -python-versions = ">=3.5" -version = "8.4.0" - -[[package]] -category = "dev" -description = "Optional static typing for Python" -name = "mypy" -optional = false -python-versions = ">=3.5" -version = "0.782" - -[package.dependencies] -mypy-extensions = ">=0.4.3,<0.5.0" -typed-ast = ">=1.4.0,<1.5.0" -typing-extensions = ">=3.7.4" - -[package.extras] -dmypy = ["psutil (>=4.0)"] - -[[package]] -category = "dev" -description = "Experimental type system extensions for programs checked with the mypy typechecker." -name = "mypy-extensions" -optional = false -python-versions = "*" -version = "0.4.3" - -[[package]] -category = "dev" -description = "Core utilities for Python packages" -name = "packaging" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "20.4" - -[package.dependencies] -pyparsing = ">=2.0.2" -six = "*" - -[[package]] -category = "main" -description = "Utility library for gitignore style pattern matching of file paths." -name = "pathspec" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.8.0" - -[[package]] -category = "dev" -description = "plugin and hook calling mechanisms for python" -name = "pluggy" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.13.1" - -[package.dependencies] -[package.dependencies.importlib-metadata] -python = "<3.8" -version = ">=0.12" - -[package.extras] -dev = ["pre-commit", "tox"] - -[[package]] -category = "dev" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -name = "py" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.9.0" - -[[package]] -category = "dev" -description = "Python parsing module" -name = "pyparsing" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -version = "2.4.7" - -[[package]] -category = "main" -description = "Python Serial Port Extension" -name = "pyserial" -optional = false -python-versions = "*" -version = "3.4" - -[[package]] -category = "dev" -description = "pytest: simple powerful testing with Python" -name = "pytest" -optional = false -python-versions = ">=3.5" -version = "5.4.3" - -[package.dependencies] -atomicwrites = ">=1.0" -attrs = ">=17.4.0" -colorama = "*" -more-itertools = ">=4.0.0" -packaging = "*" -pluggy = ">=0.12,<1.0" -py = ">=1.5.0" -wcwidth = "*" - -[package.dependencies.importlib-metadata] -python = "<3.8" -version = ">=0.12" - -[package.extras] -checkqa-mypy = ["mypy (v0.761)"] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] - -[[package]] -category = "main" -description = "Alternative regular expression module, to replace re." -name = "regex" -optional = false -python-versions = "*" -version = "2020.7.14" - -[[package]] -category = "dev" -description = "Python 2 and 3 compatibility utilities" -name = "six" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -version = "1.15.0" - -[[package]] -category = "main" -description = "Python Library for Tom's Obvious, Minimal Language" -name = "toml" -optional = false -python-versions = "*" -version = "0.10.1" - -[[package]] -category = "main" -description = "a fork of Python 2 and 3 ast modules with type comment support" -name = "typed-ast" -optional = false -python-versions = "*" -version = "1.4.1" - -[[package]] -category = "dev" -description = "Backported and Experimental Type Hints for Python 3.5+" -name = "typing-extensions" -optional = false -python-versions = "*" -version = "3.7.4.2" - -[[package]] -category = "dev" -description = "Measures the displayed width of unicode strings in a terminal" -name = "wcwidth" -optional = false -python-versions = "*" -version = "0.2.5" - -[[package]] -category = "dev" -description = "Backport of pathlib-compatible object wrapper for zip files" -marker = "python_version < \"3.8\"" -name = "zipp" -optional = false -python-versions = ">=3.6" -version = "3.1.0" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["jaraco.itertools", "func-timeout"] - -[metadata] -content-hash = "e591cefbf7f181c5a3dbb815804404256412aa148b25f12528bbad5dfb31e6e7" -python-versions = "^3.7" - -[metadata.files] -appdirs = [ - {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, - {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, -] -atomicwrites = [ - {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, - {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, -] -attrs = [ - {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, - {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, -] -black = [ - {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"}, - {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"}, -] -click = [ - {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, - {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, -] -colorama = [ - {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, - {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, -] -importlib-metadata = [ - {file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"}, - {file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"}, -] -more-itertools = [ - {file = "more-itertools-8.4.0.tar.gz", hash = "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5"}, - {file = "more_itertools-8.4.0-py3-none-any.whl", hash = "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2"}, -] -mypy = [ - {file = "mypy-0.782-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:2c6cde8aa3426c1682d35190b59b71f661237d74b053822ea3d748e2c9578a7c"}, - {file = "mypy-0.782-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9c7a9a7ceb2871ba4bac1cf7217a7dd9ccd44c27c2950edbc6dc08530f32ad4e"}, - {file = "mypy-0.782-cp35-cp35m-win_amd64.whl", hash = "sha256:c05b9e4fb1d8a41d41dec8786c94f3b95d3c5f528298d769eb8e73d293abc48d"}, - {file = "mypy-0.782-cp36-cp36m-macosx_10_6_x86_64.whl", hash = "sha256:6731603dfe0ce4352c555c6284c6db0dc935b685e9ce2e4cf220abe1e14386fd"}, - {file = "mypy-0.782-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:f05644db6779387ccdb468cc47a44b4356fc2ffa9287135d05b70a98dc83b89a"}, - {file = "mypy-0.782-cp36-cp36m-win_amd64.whl", hash = "sha256:b7fbfabdbcc78c4f6fc4712544b9b0d6bf171069c6e0e3cb82440dd10ced3406"}, - {file = "mypy-0.782-cp37-cp37m-macosx_10_6_x86_64.whl", hash = "sha256:3fdda71c067d3ddfb21da4b80e2686b71e9e5c72cca65fa216d207a358827f86"}, - {file = "mypy-0.782-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d7df6eddb6054d21ca4d3c6249cae5578cb4602951fd2b6ee2f5510ffb098707"}, - {file = "mypy-0.782-cp37-cp37m-win_amd64.whl", hash = "sha256:a4a2cbcfc4cbf45cd126f531dedda8485671545b43107ded25ce952aac6fb308"}, - {file = "mypy-0.782-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6bb93479caa6619d21d6e7160c552c1193f6952f0668cdda2f851156e85186fc"}, - {file = "mypy-0.782-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:81c7908b94239c4010e16642c9102bfc958ab14e36048fa77d0be3289dda76ea"}, - {file = "mypy-0.782-cp38-cp38-win_amd64.whl", hash = "sha256:5dd13ff1f2a97f94540fd37a49e5d255950ebcdf446fb597463a40d0df3fac8b"}, - {file = "mypy-0.782-py3-none-any.whl", hash = "sha256:e0b61738ab504e656d1fe4ff0c0601387a5489ca122d55390ade31f9ca0e252d"}, - {file = "mypy-0.782.tar.gz", hash = "sha256:eff7d4a85e9eea55afa34888dfeaccde99e7520b51f867ac28a48492c0b1130c"}, -] -mypy-extensions = [ - {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, - {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, -] -packaging = [ - {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, - {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, -] -pathspec = [ - {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"}, - {file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"}, -] -pluggy = [ - {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, - {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, -] -py = [ - {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"}, - {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, -] -pyparsing = [ - {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, - {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, -] -pyserial = [ - {file = "pyserial-3.4-py2.py3-none-any.whl", hash = "sha256:e0770fadba80c31013896c7e6ef703f72e7834965954a78e71a3049488d4d7d8"}, - {file = "pyserial-3.4.tar.gz", hash = "sha256:6e2d401fdee0eab996cf734e67773a0143b932772ca8b42451440cfed942c627"}, -] -pytest = [ - {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, - {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, -] -regex = [ - {file = "regex-2020.7.14-cp27-cp27m-win32.whl", hash = "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"}, - {file = "regex-2020.7.14-cp27-cp27m-win_amd64.whl", hash = "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644"}, - {file = "regex-2020.7.14-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc"}, - {file = "regex-2020.7.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067"}, - {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd"}, - {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88"}, - {file = "regex-2020.7.14-cp36-cp36m-win32.whl", hash = "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4"}, - {file = "regex-2020.7.14-cp36-cp36m-win_amd64.whl", hash = "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f"}, - {file = "regex-2020.7.14-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162"}, - {file = "regex-2020.7.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf"}, - {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7"}, - {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89"}, - {file = "regex-2020.7.14-cp37-cp37m-win32.whl", hash = "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6"}, - {file = "regex-2020.7.14-cp37-cp37m-win_amd64.whl", hash = "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204"}, - {file = "regex-2020.7.14-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99"}, - {file = "regex-2020.7.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e"}, - {file = "regex-2020.7.14-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e"}, - {file = "regex-2020.7.14-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a"}, - {file = "regex-2020.7.14-cp38-cp38-win32.whl", hash = "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341"}, - {file = "regex-2020.7.14-cp38-cp38-win_amd64.whl", hash = "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840"}, - {file = "regex-2020.7.14.tar.gz", hash = "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb"}, -] -six = [ - {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, - {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, -] -toml = [ - {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, - {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, -] -typed-ast = [ - {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, - {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, - {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, - {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, - {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, - {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, - {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, - {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, - {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, - {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, - {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, - {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, - {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, - {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, - {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, - {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, - {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, - {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, - {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, - {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, - {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, -] -typing-extensions = [ - {file = "typing_extensions-3.7.4.2-py2-none-any.whl", hash = "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392"}, - {file = "typing_extensions-3.7.4.2-py3-none-any.whl", hash = "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5"}, - {file = "typing_extensions-3.7.4.2.tar.gz", hash = "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae"}, -] -wcwidth = [ - {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, - {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, -] -zipp = [ - {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, - {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"}, -] From 23a2c08425a2e43d85d62e6b960a387b200394a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Grilo?= Date: Wed, 19 Mar 2025 16:51:22 +0000 Subject: [PATCH 041/267] Update .gitignore and README --- .gitignore | 180 +++++++++++++++++++++++++++++++++++++++++++++++++++-- README.md | 3 + 2 files changed, 177 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index f60ee92..1800114 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,174 @@ -.idea/ -pyharp.egg-info/ -.python-version -__pycache__ -tests/.pytest_cache -**/*.bin \ No newline at end of file +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc \ No newline at end of file diff --git a/README.md b/README.md index b506d9b..9191782 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ Harp implementation of the Harp protocol. +> [!CAUTION] +> The README is currently outdated! + ## Install with Pip From this directory, install in editable mode with ```` From b121ab125f02272c2e92c33dab7961c9fe5e6900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Grilo?= Date: Mon, 24 Mar 2025 11:07:26 +0000 Subject: [PATCH 042/267] Chore: remove shebangs from example scripts --- examples/behavior_device_driver_test.py | 48 +++++++++++------------ examples/check_device_id.py | 18 ++++----- examples/get_info.py | 36 ++++++++--------- examples/wait_for_events.py | 25 ++++++------ examples/write_and_read_from_registers.py | 36 ++++++++--------- 5 files changed, 78 insertions(+), 85 deletions(-) diff --git a/examples/behavior_device_driver_test.py b/examples/behavior_device_driver_test.py index d9cfab7..9971ad6 100755 --- a/examples/behavior_device_driver_test.py +++ b/examples/behavior_device_driver_test.py @@ -1,49 +1,47 @@ -#!/usr/bin/env python3 -from pyharp.drivers.behavior import Behavior -from pyharp.messages import HarpMessage -from pyharp.messages import MessageType -from struct import * import os +from struct import * +from pyharp.drivers.behavior import Behavior +from pyharp.messages import HarpMessage, MessageType # Open the device and print the info on screen # Open serial connection and save communication to a file device = None -if os.name == 'posix': # check for Linux. +if os.name == "posix": # check for Linux. device = Behavior("/dev/harp_device_00", "ibl.bin") -else: # assume Windows. +else: # assume Windows. device = Behavior("COM95", "ibl.bin") print(f"digital inputs: {device.all_input_states:03b}") print(f"digital outputs: {device.all_output_states:016b}") print(f"setting digital outputs") -#device.all_output_states = 0x0000 # Set the whole port directly. -#device.set_outputs(0xFFFF) # Set the values set to logic 1 only. -#device.clear_outputs(0xFFFF)# Clear values set to logic 1 only. +# device.all_output_states = 0x0000 # Set the whole port directly. +# device.set_outputs(0xFFFF) # Set the values set to logic 1 only. +# device.clear_outputs(0xFFFF)# Clear values set to logic 1 only. print(f"digital outputs: {device.all_output_states:016b}") device.set_io_configuration(0b111) # TODO: FIXME. IOs are not working -#device.set_io_configuration(0b111) # This is getting ignored? -#device.set_io_outputs(0b000) -#device.all_io_states = 0b000 -#print(f"digital ios: {device.all_io_states:03b}") +# device.set_io_configuration(0b111) # This is getting ignored? +# device.set_io_outputs(0b000) +# device.all_io_states = 0b000 +# print(f"digital ios: {device.all_io_states:03b}") -#device.D0 = 0 -#print(f"D0: {device.D0}") -#device.D0 = 1 -#print(f"D0: {device.D0}") +# device.D0 = 0 +# print(f"D0: {device.D0}") +# device.D0 = 1 +# print(f"D0: {device.D0}") # -#device.D1 = 0 -#print(f"D1: {device.D1}") -#device.D1 = 1 -#print(f"D1: {device.D1}") +# device.D1 = 0 +# print(f"D1: {device.D1}") +# device.D1 = 1 +# print(f"D1: {device.D1}") # -#print(f"DI2: {device.DI2}") +# print(f"DI2: {device.DI2}") -#import time -#while True: +# import time +# while True: # print(f"PORT0 IN State: {device.port0_i0}") # print(f"PORT0 IO State: {device.port0_io0}") # print(f"PORT0 OUT State: {device.port0_o0}") diff --git a/examples/check_device_id.py b/examples/check_device_id.py index 9568921..7d94eb3 100755 --- a/examples/check_device_id.py +++ b/examples/check_device_id.py @@ -1,11 +1,9 @@ -#!/usr/bin/env python3 -from pyharp.device import Device -from pyharp.messages import HarpMessage -from pyharp.messages import MessageType -from pyharp.device_names import device_names -from struct import * import os +from struct import * +from pyharp.device import Device +from pyharp.device_names import device_names +from pyharp.messages import HarpMessage, MessageType # ON THIS EXAMPLE # @@ -15,15 +13,15 @@ # Open the device # Open serial connection -if os.name == "posix": # check for Linux. +if os.name == "posix": # check for Linux. device = Device("/dev/harp_device_00") -else: # assume Windows. +else: # assume Windows. device = Device("COM95") # Get some of the device's parameters -device_id = device.WHO_AM_I # Get device's ID +device_id = device.WHO_AM_I # Get device's ID device_id_description = device.WHO_AM_I_DEVICE # Get device's user name -device_user_name = device.DEVICE_NAME # Get device's user name +device_user_name = device.DEVICE_NAME # Get device's user name # Check if we are dealing with the correct device if device_id in device_names: diff --git a/examples/get_info.py b/examples/get_info.py index eec8cef..e0f2ff6 100755 --- a/examples/get_info.py +++ b/examples/get_info.py @@ -1,10 +1,8 @@ -#!/usr/bin/env python3 -from pyharp.device import Device, DeviceMode -from pyharp.messages import HarpMessage -from pyharp.messages import MessageType -from struct import * import os +from struct import * +from pyharp.device import Device, DeviceMode +from pyharp.messages import HarpMessage, MessageType # ON THIS EXAMPLE # @@ -14,27 +12,27 @@ # Open the device and print the info on screen # Open serial connection and save communication to a file -if os.name == 'posix': # check for Linux. - #device = Device("/dev/harp_device_00", "ibl.bin") - #device = Device("/dev/ttyACM0") +if os.name == "posix": # check for Linux. + # device = Device("/dev/harp_device_00", "ibl.bin") + # device = Device("/dev/ttyACM0") device = Device("/dev/ttyUSB0") -else: # assume Windows. +else: # assume Windows. device = Device("COM95", "ibl.bin") -device.info() # Display device's info on screen +device.info() # Display device's info on screen # Get some of the device's parameters -device_id = device.WHO_AM_I # Get device's ID +device_id = device.WHO_AM_I # Get device's ID device_id_description = device.WHO_AM_I_DEVICE # Get device's user name -device_user_name = device.DEVICE_NAME # Get device's user name +device_user_name = device.DEVICE_NAME # Get device's user name # Get versions -device_fw_h = device.FIRMWARE_VERSION_H # Get device's firmware version -device_fw_l = device.FIRMWARE_VERSION_L # Get device's firmware version -device_hw_h = device.HW_VERSION_H # Get device's hardware version -device_hw_l = device.HW_VERSION_L # Get device's hardware version -device_harp_h = device.HARP_VERSION_H # Get device's harp core version -device_harp_l = device.HARP_VERSION_L # Get device's harp core version -device_assembly = device.ASSEMBLY_VERSION # Get device's assembly version +device_fw_h = device.FIRMWARE_VERSION_H # Get device's firmware version +device_fw_l = device.FIRMWARE_VERSION_L # Get device's firmware version +device_hw_h = device.HW_VERSION_H # Get device's hardware version +device_hw_l = device.HW_VERSION_L # Get device's hardware version +device_harp_h = device.HARP_VERSION_H # Get device's harp core version +device_harp_l = device.HARP_VERSION_L # Get device's harp core version +device_assembly = device.ASSEMBLY_VERSION # Get device's assembly version reg_dump = device.dump_registers() for reg_reply in reg_dump: diff --git a/examples/wait_for_events.py b/examples/wait_for_events.py index 584836e..27f693e 100755 --- a/examples/wait_for_events.py +++ b/examples/wait_for_events.py @@ -1,28 +1,27 @@ -#!/usr/bin/env python3 -from pyharp.drivers.behavior import Behavior, Events -from pyharp.messages import HarpMessage -from pyharp.messages import MessageType -from struct import * import os +from struct import * from pyharp.device import Device, DeviceMode - +from pyharp.drivers.behavior import Behavior, Events +from pyharp.messages import HarpMessage, MessageType # Open the device and print the info on screen # Open serial connection and save communication to a file device = None -if os.name == 'posix': # check for Linux. - #device = Behavior("/dev/harp_device_00", "ibl.bin") - #device = Device("/dev/ttyACM0",) - device = Device("/dev/ttyUSB0",) -else: # assume Windows. +if os.name == "posix": # check for Linux. + # device = Behavior("/dev/harp_device_00", "ibl.bin") + # device = Device("/dev/ttyACM0",) + device = Device( + "/dev/ttyUSB0", + ) +else: # assume Windows. device = Behavior("COM95", "ibl.bin") print("Setting mode to active.") # Mode will remain active for up to 3 seconds after CTS pin is brought low. device.set_mode(DeviceMode.Active) -#device.disable_all_events() -#device.enable_events(Events.port_digital_inputs) +# device.disable_all_events() +# device.enable_events(Events.port_digital_inputs) while True: for msg in device.get_events(): print(msg) diff --git a/examples/write_and_read_from_registers.py b/examples/write_and_read_from_registers.py index 4430b00..d02aab0 100755 --- a/examples/write_and_read_from_registers.py +++ b/examples/write_and_read_from_registers.py @@ -1,10 +1,8 @@ -#!/usr/bin/env python3 -from pyharp.device import Device -from pyharp.messages import HarpMessage -from pyharp.messages import MessageType -from struct import * import os +from struct import * +from pyharp.device import Device +from pyharp.messages import HarpMessage, MessageType # ON THIS EXAMPLE # @@ -15,21 +13,21 @@ # Open the device and print the info on screen # Open serial connection and save communication to a file -if os.name == 'posix': # check for Linux. +if os.name == "posix": # check for Linux. device = Device("/dev/harp_device_00", "ibl.bin") -else: # assume Windows. +else: # assume Windows. device = Device("COM95", "ibl.bin") # Read current analog sensor's higher threshold (ANA_SENSOR_TH0_HIGH) at address 42 -#analog_threshold_h = device.send(HarpMessage.ReadU16(42).frame).payload_as_int() -#print(f"Analog sensor's higher threshold: {analog_threshold_h}") +# analog_threshold_h = device.send(HarpMessage.ReadU16(42).frame).payload_as_int() +# print(f"Analog sensor's higher threshold: {analog_threshold_h}") import time print(f"System time: {time.perf_counter():.6f}") -data_stream = device.send(HarpMessage.ReadU8(33).frame) # returns a ReplyHarpMessage -#data_stream = device.send(HarpMessage.ReadS16(33).frame).payload_as_int_array() +data_stream = device.send(HarpMessage.ReadU8(33).frame) # returns a ReplyHarpMessage +# data_stream = device.send(HarpMessage.ReadS16(33).frame).payload_as_int_array() print(f"Data Stream payload type: {data_stream.payload_type.name}") print(f"Data Stream message type: {data_stream.message_type.name}") print(f"Data Stream timestamp: {data_stream.timestamp}") @@ -37,7 +35,9 @@ print(f"Data Stream payload: {data_stream.payload}") print(f"System time: {time.perf_counter():.6f}") -event_reg_response = device.send(HarpMessage.ReadU8(77).frame) # returns a ReplyHarpMessage +event_reg_response = device.send( + HarpMessage.ReadU8(77).frame +) # returns a ReplyHarpMessage print(f"EVNT_ENABLE payload type: {event_reg_response.payload_type.name}") print(f"EVNT_ENABLE message type: {event_reg_response.message_type.name}") print(f"EVNT_ENABLE timestamp: {event_reg_response.timestamp}") @@ -45,19 +45,19 @@ print(f"EVNT_ENABLE payload: {event_reg_response.payload[0]:08b}") ## Increase current analog sensor's higher threshold by one unit -#device.send(HarpMessage.WriteU16(42, analog_threshold_h+1).frame) +# device.send(HarpMessage.WriteU16(42, analog_threshold_h+1).frame) # ## Check if the register was well written -#analog_threshold_h = device.send(HarpMessage.ReadU16(42).frame).payload_as_int() -#print(f"Analog sensor's higher threshold: {analog_threshold_h}") +# analog_threshold_h = device.send(HarpMessage.ReadU16(42).frame).payload_as_int() +# print(f"Analog sensor's higher threshold: {analog_threshold_h}") # ## Read 10 samples of the analog sensor and display the values ## The value is at register STREAM[0], address 33 -#analog_sensor = [] -#for x in range(10): +# analog_sensor = [] +# for x in range(10): # value = device.send(HarpMessage.ReadS16(33).frame).payload_as_int() # analog_sensor.append(value & 0xffff) -#print(f"Analog sensor's values: {analog_sensor}") +# print(f"Analog sensor's values: {analog_sensor}") # Close connection device.disconnect() From 3f1b5f88ea5c6d8ccd4046f4b696fd262ec368e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Tue, 25 Mar 2025 10:40:00 +0000 Subject: [PATCH 043/267] Cleanup unused code --- examples/behavior_device_driver_test.py | 2 -- examples/check_device_id.py | 2 -- examples/get_info.py | 4 +--- examples/wait_for_events.py | 4 +--- examples/write_and_read_from_registers.py | 3 +-- pyharp/main.py | 9 --------- tests/test_device.py | 5 ++--- tests/test_messages.py | 2 +- 8 files changed, 6 insertions(+), 25 deletions(-) delete mode 100644 pyharp/main.py diff --git a/examples/behavior_device_driver_test.py b/examples/behavior_device_driver_test.py index 9971ad6..24c1978 100755 --- a/examples/behavior_device_driver_test.py +++ b/examples/behavior_device_driver_test.py @@ -1,8 +1,6 @@ import os -from struct import * from pyharp.drivers.behavior import Behavior -from pyharp.messages import HarpMessage, MessageType # Open the device and print the info on screen # Open serial connection and save communication to a file diff --git a/examples/check_device_id.py b/examples/check_device_id.py index 7d94eb3..7c6bd57 100755 --- a/examples/check_device_id.py +++ b/examples/check_device_id.py @@ -1,9 +1,7 @@ import os -from struct import * from pyharp.device import Device from pyharp.device_names import device_names -from pyharp.messages import HarpMessage, MessageType # ON THIS EXAMPLE # diff --git a/examples/get_info.py b/examples/get_info.py index e0f2ff6..8a33174 100755 --- a/examples/get_info.py +++ b/examples/get_info.py @@ -1,8 +1,6 @@ import os -from struct import * -from pyharp.device import Device, DeviceMode -from pyharp.messages import HarpMessage, MessageType +from pyharp.device import Device # ON THIS EXAMPLE # diff --git a/examples/wait_for_events.py b/examples/wait_for_events.py index 27f693e..6cdb615 100755 --- a/examples/wait_for_events.py +++ b/examples/wait_for_events.py @@ -1,9 +1,7 @@ import os -from struct import * from pyharp.device import Device, DeviceMode -from pyharp.drivers.behavior import Behavior, Events -from pyharp.messages import HarpMessage, MessageType +from pyharp.drivers.behavior import Behavior # Open the device and print the info on screen # Open serial connection and save communication to a file diff --git a/examples/write_and_read_from_registers.py b/examples/write_and_read_from_registers.py index d02aab0..e17ba1d 100755 --- a/examples/write_and_read_from_registers.py +++ b/examples/write_and_read_from_registers.py @@ -1,8 +1,7 @@ import os -from struct import * from pyharp.device import Device -from pyharp.messages import HarpMessage, MessageType +from pyharp.messages import HarpMessage # ON THIS EXAMPLE # diff --git a/pyharp/main.py b/pyharp/main.py deleted file mode 100644 index 7197906..0000000 --- a/pyharp/main.py +++ /dev/null @@ -1,9 +0,0 @@ -import serial - - -def main() -> None: - print("hello world") - - -if __name__ == "__main__": - main() diff --git a/tests/test_device.py b/tests/test_device.py index 5a5eab9..cc5dff2 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -1,8 +1,7 @@ -import serial import time -from typing import Optional -from pyharp.messages import HarpMessage, ReplyHarpMessage + from pyharp.device import Device +from pyharp.messages import HarpMessage, ReplyHarpMessage DEFAULT_ADDRESS = 42 diff --git a/tests/test_messages.py b/tests/test_messages.py index 1100ae9..452bfed 100644 --- a/tests/test_messages.py +++ b/tests/test_messages.py @@ -1,5 +1,5 @@ from pyharp.messages import HarpMessage -from pyharp.messages import MessageType +from pyharp.messages import HarpMessage, MessageType from pyharp.messages import CommonRegisters DEFAULT_ADDRESS = 42 From 2e4f2169bafccf5085349b8f0ea0c1df845839a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Tue, 25 Mar 2025 10:41:51 +0000 Subject: [PATCH 044/267] Remove ReadXXHarpMessage classes --- pyharp/messages.py | 64 ++++++++++------------------------------------ 1 file changed, 14 insertions(+), 50 deletions(-) diff --git a/pyharp/messages.py b/pyharp/messages.py index e86747a..cdf8ff1 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -102,34 +102,32 @@ def checksum(self) -> int: return self._frame[-1] @staticmethod - def ReadU8(address: int) -> ReadU8HarpMessage: - return ReadU8HarpMessage(address) + def ReadU8(address: int) -> ReadHarpMessage: + return ReadHarpMessage(payload_type=PayloadType.U8, address=address) @staticmethod - def ReadS8(address: int) -> ReadS8HarpMessage: - return ReadS8HarpMessage(address) + def ReadS8(address: int) -> ReadHarpMessage: + return ReadHarpMessage(payload_type=PayloadType.S8, address=address) @staticmethod - def ReadS16(address: int) -> ReadS16HarpMessage: - return ReadS16HarpMessage(address) + def ReadS16(address: int) -> ReadHarpMessage: + return ReadHarpMessage(payload_type=PayloadType.S16, address=address) @staticmethod - def ReadU16(address: int) -> ReadU16HarpMessage: - return ReadU16HarpMessage(address) - - # TODO: ReadS16 + def ReadU16(address: int) -> ReadHarpMessage: + return ReadHarpMessage(payload_type=PayloadType.U16, address=address) @staticmethod - def ReadU32(address: int) -> ReadU32HarpMessage: - return ReadU32HarpMessage(address) + def ReadU32(address: int) -> ReadHarpMessage: + return ReadHarpMessage(payload_type=PayloadType.U32, address=address) @staticmethod - def ReadS32(address: int) -> ReadS32HarpMessage: - return ReadS32HarpMessage(address) + def ReadS32(address: int) -> ReadHarpMessage: + return ReadHarpMessage(payload_type=PayloadType.S32, address=address) @staticmethod - def ReadFloat(address: int) -> ReadFloatHarpMessage: - return ReadFloatHarpMessage(address) + def ReadFloat(address: int) -> ReadHarpMessage: + return ReadHarpMessage(payload_type=PayloadType.Float, address=address) @staticmethod def WriteU8(address: int, value: int) -> WriteU8HarpMessage: @@ -268,40 +266,6 @@ def __init__(self, payload_type: PayloadType, address: int): self._frame.append(self.calculate_checksum()) -class ReadU8HarpMessage(ReadHarpMessage): - def __init__(self, address: int): - super().__init__(PayloadType.U8, address) - - -class ReadS8HarpMessage(ReadHarpMessage): - def __init__(self, address: int): - super().__init__(PayloadType.S8, address) - - -class ReadU16HarpMessage(ReadHarpMessage): - def __init__(self, address: int): - super().__init__(PayloadType.U16, address) - - -class ReadS16HarpMessage(ReadHarpMessage): - def __init__(self, address: int): - super().__init__(PayloadType.S16, address) - -class ReadU32HarpMessage(ReadHarpMessage): - def __init__(self, address: int): - super().__init__(PayloadType.U32, address) - - -class ReadS32HarpMessage(ReadHarpMessage): - def __init__(self, address: int): - super().__init__(PayloadType.S32, address) - - -class ReadFloatHarpMessage(ReadHarpMessage): - def __init__(self, address: int): - super().__init__(PayloadType.Float, address) - - class WriteHarpMessage(HarpMessage): BASE_LENGTH: int = 5 MESSAGE_TYPE: int = MessageType.WRITE From 00217d4d2016448a42df7dd88d3f050b469158d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Tue, 25 Mar 2025 10:44:09 +0000 Subject: [PATCH 045/267] Move base classes --- pyharp/base.py | 52 +++++++++++++++++++++++++++++++++++++ pyharp/device.py | 15 ++++++----- pyharp/messages.py | 58 +++--------------------------------------- tests/test_messages.py | 3 +-- 4 files changed, 65 insertions(+), 63 deletions(-) create mode 100644 pyharp/base.py diff --git a/pyharp/base.py b/pyharp/base.py new file mode 100644 index 0000000..70daabd --- /dev/null +++ b/pyharp/base.py @@ -0,0 +1,52 @@ +from enum import Enum + + +class MessageType(Enum): + READ: int = 1 + WRITE: int = 2 + EVENT: int = 3 + READ_ERROR: int = 9 + WRITE_ERROR: int = 10 + + +class PayloadType(Enum): + isUnsigned: int = 0x00 + isSigned: int = 0x80 + isFloat: int = 0x40 + hasTimestamp: int = 0x10 + + U8 = isUnsigned | 1 # 1 + S8 = isSigned | 1 # 129 + U16 = isUnsigned | 2 # 2 + S16 = isSigned | 2 # 130 + U32 = isUnsigned | 4 + S32 = isSigned | 4 + U64 = isUnsigned | 8 + S64 = isSigned | 8 + Float = isFloat | 4 + Timestamp = hasTimestamp + TimestampedU8 = hasTimestamp | U8 + TimestampedS8 = hasTimestamp | S8 + TimestampedU16 = hasTimestamp | U16 + TimestampedS16 = hasTimestamp | S16 + TimestampedU32 = hasTimestamp | U32 + TimestampedS32 = hasTimestamp | S32 + TimestampedU64 = hasTimestamp | U64 + TimestampedS64 = hasTimestamp | S64 + TimestampedFloat = hasTimestamp | Float + + +class CommonRegisters: + WHO_AM_I = 0x00 + HW_VERSION_H = 0x01 + HW_VERSION_L = 0x02 + ASSEMBLY_VERSION = 0x03 + HARP_VERSION_H = 0x04 + HARP_VERSION_L = 0x05 + FIRMWARE_VERSION_H = 0x06 + FIRMWARE_VERSION_L = 0x07 + TIMESTAMP_SECOND = 0x08 + TIMESTAMP_MICRO = 0x09 + OPERATION_CTRL = 0x0A + RESET_DEV = 0x0B + DEVICE_NAME = 0x0C diff --git a/pyharp/device.py b/pyharp/device.py index b4c6f6d..fa06a26 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -1,16 +1,17 @@ -from __future__ import annotations # enable subscriptable type hints for lists. -import serial +from __future__ import annotations # enable subscriptable type hints for lists. + import logging import queue -from typing import Optional, Union +from enum import Enum from pathlib import Path +from typing import Optional, Union +import serial + +from pyharp.base import CommonRegisters +from pyharp.device_names import device_names from pyharp.harp_serial import HarpSerial from pyharp.messages import HarpMessage, ReplyHarpMessage -from pyharp.messages import CommonRegisters, MessageType -from pyharp.device_names import device_names -from enum import Enum -from time import perf_counter class DeviceMode(Enum): diff --git a/pyharp/messages.py b/pyharp/messages.py index cdf8ff1..75ea243 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -1,59 +1,9 @@ -from __future__ import annotations # for type hints (PEP 563) -from enum import Enum -# from abc import ABC, abstractmethod -from typing import Union, Tuple, Optional, List -import struct +from __future__ import annotations # for type hints (PEP 563) +import struct +from typing import List, Union -class MessageType(Enum): - READ: int = 1 - WRITE: int = 2 - EVENT: int = 3 - READ_ERROR: int = 9 - WRITE_ERROR: int = 10 - - -class PayloadType(Enum): - isUnsigned: int = 0x00 - isSigned: int = 0x80 - isFloat: int = 0x40 - hasTimestamp: int = 0x10 - - U8 = isUnsigned | 1 # 1 - S8 = isSigned | 1 # 129 - U16 = isUnsigned | 2 # 2 - S16 = isSigned | 2 # 130 - U32 = isUnsigned | 4 - S32 = isSigned | 4 - U64 = isUnsigned | 8 - S64 = isSigned | 8 - Float = isFloat | 4 - Timestamp = hasTimestamp - TimestampedU8 = hasTimestamp | U8 - TimestampedS8 = hasTimestamp | S8 - TimestampedU16 = hasTimestamp | U16 - TimestampedS16 = hasTimestamp | S16 - TimestampedU32 = hasTimestamp | U32 - TimestampedS32 = hasTimestamp | S32 - TimestampedU64 = hasTimestamp | U64 - TimestampedS64 = hasTimestamp | S64 - TimestampedFloat = hasTimestamp | Float - - -class CommonRegisters: - WHO_AM_I = 0x00 - HW_VERSION_H = 0x01 - HW_VERSION_L = 0x02 - ASSEMBLY_VERSION = 0x03 - HARP_VERSION_H = 0x04 - HARP_VERSION_L = 0x05 - FIRMWARE_VERSION_H = 0x06 - FIRMWARE_VERSION_L = 0x07 - TIMESTAMP_SECOND = 0x08 - TIMESTAMP_MICRO = 0x09 - OPERATION_CTRL = 0x0A - RESET_DEV = 0x0B - DEVICE_NAME = 0x0C +from pyharp.base import MessageType, PayloadType class HarpMessage: diff --git a/tests/test_messages.py b/tests/test_messages.py index 452bfed..a5e1ae3 100644 --- a/tests/test_messages.py +++ b/tests/test_messages.py @@ -1,6 +1,5 @@ -from pyharp.messages import HarpMessage +from pyharp.base import CommonRegisters from pyharp.messages import HarpMessage, MessageType -from pyharp.messages import CommonRegisters DEFAULT_ADDRESS = 42 From 2716ce81209c864ee110c9aa24bf0124cb3bf93a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Tue, 25 Mar 2025 10:46:48 +0000 Subject: [PATCH 046/267] Formatting --- pyharp/messages.py | 61 ++++++++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/pyharp/messages.py b/pyharp/messages.py index 75ea243..9e07b94 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -114,10 +114,9 @@ def parse(frame: bytearray) -> ReplyHarpMessage: # A Response Message from a harp device. class ReplyHarpMessage(HarpMessage): - - def __init__( - self, frame: bytearray, + self, + frame: bytearray, ): """ @@ -127,57 +126,66 @@ def __init__( self._frame = frame # retrieve all content from 11 (where payload starts) until the checksum (not inclusive) self._raw_payload = frame[11:-1] - self._payload = self._parse_payload(self._raw_payload) # payload formatted as list[payload type] + self._payload = self._parse_payload( + self._raw_payload + ) # payload formatted as list[payload type] # Assign timestamp after _payload since @properties all rely on self._payload. - self._timestamp = int.from_bytes(frame[5:9], byteorder="little", signed=False) + \ - int.from_bytes(frame[9:11], byteorder="little", signed=False)*32e-6 + self._timestamp = ( + int.from_bytes(frame[5:9], byteorder="little", signed=False) + + int.from_bytes(frame[9:11], byteorder="little", signed=False) * 32e-6 + ) # Timestamp is junk if it's not present. if not (self.payload_type.value & PayloadType.hasTimestamp.value): self._timestamp = None - def _parse_payload(self, raw_payload) -> list[int]: """return the payload as a list of ints after parsing it from the raw payload.""" is_signed = True if (self.payload_type.value & 0x80) else False is_float = True if (self.payload_type.value & 0x40) else False bytes_per_word = self.payload_type.value & 0x07 - payload_len = len(raw_payload) # payload length in bytes. + payload_len = len(raw_payload) # payload length in bytes. - word_chunks = [raw_payload[i:i+bytes_per_word] for i in range(0, payload_len, bytes_per_word)] + word_chunks = [ + raw_payload[i : i + bytes_per_word] + for i in range(0, payload_len, bytes_per_word) + ] if not is_float: - return [int.from_bytes(chunk, byteorder="little", signed=is_signed) for chunk in word_chunks] - else: # handle float case. - return [struct.unpack(' Union[int, list[int]]: @@ -202,7 +210,6 @@ def payload_as_float(self) -> float: class ReadHarpMessage(HarpMessage): MESSAGE_TYPE: int = MessageType.READ - def __init__(self, payload_type: PayloadType, address: int): self._frame = bytearray() @@ -237,7 +244,7 @@ def __init__( self._frame.append(self.BASE_LENGTH + offset) self._frame.append(address) - self._frame.append(HarpMessage.DEFAULT_PORT) + self._frame.append(self.DEFAULT_PORT) self._frame.append(payload_type.value) # Handle payloads that are bytes or bytearray (bytearray = multi-motor instructions) From 0f57b08724fbac03bcc1b7b3e48148efcf673e07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Tue, 25 Mar 2025 10:47:00 +0000 Subject: [PATCH 047/267] Update protocol link --- pyharp/messages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyharp/messages.py b/pyharp/messages.py index 9e07b94..d8f683b 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -8,7 +8,7 @@ class HarpMessage: """ - https://github.com/harp-tech/protocol/blob/master/Binary%20Protocol%201.0%201.1%2020180223.pdf + https://github.com/harp-tech/protocol/blob/main/BinaryProtocol-8bit.md """ DEFAULT_PORT: int = 255 From 9b86a4508e43ad0d9e0c607e19028d3f9246a51a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Tue, 25 Mar 2025 11:30:54 +0000 Subject: [PATCH 048/267] Convert usage of ReadXX classes to device related methods --- examples/behavior_device_driver_test.py | 4 +- examples/write_and_read_from_registers.py | 11 +-- pyharp/device.py | 107 +++++++++++----------- pyharp/drivers/behavior.py | 27 +++--- tests/test_device.py | 6 +- tests/test_messages.py | 16 ++-- 6 files changed, 89 insertions(+), 82 deletions(-) diff --git a/examples/behavior_device_driver_test.py b/examples/behavior_device_driver_test.py index 24c1978..dd6a3a4 100755 --- a/examples/behavior_device_driver_test.py +++ b/examples/behavior_device_driver_test.py @@ -6,7 +6,7 @@ # Open serial connection and save communication to a file device = None if os.name == "posix": # check for Linux. - device = Behavior("/dev/harp_device_00", "ibl.bin") + device = Behavior("/dev/ttyUSB0", "ibl.bin") else: # assume Windows. device = Behavior("COM95", "ibl.bin") @@ -17,7 +17,7 @@ # device.set_outputs(0xFFFF) # Set the values set to logic 1 only. # device.clear_outputs(0xFFFF)# Clear values set to logic 1 only. print(f"digital outputs: {device.all_output_states:016b}") -device.set_io_configuration(0b111) +# device.set_io_configuration(0b111) # TODO: FIXME. IOs are not working # device.set_io_configuration(0b111) # This is getting ignored? diff --git a/examples/write_and_read_from_registers.py b/examples/write_and_read_from_registers.py index e17ba1d..91d7c7e 100755 --- a/examples/write_and_read_from_registers.py +++ b/examples/write_and_read_from_registers.py @@ -1,7 +1,6 @@ import os from pyharp.device import Device -from pyharp.messages import HarpMessage # ON THIS EXAMPLE # @@ -13,7 +12,7 @@ # Open the device and print the info on screen # Open serial connection and save communication to a file if os.name == "posix": # check for Linux. - device = Device("/dev/harp_device_00", "ibl.bin") + device = Device("/dev/ttyUSB0", "ibl.bin") else: # assume Windows. device = Device("COM95", "ibl.bin") @@ -25,8 +24,8 @@ import time print(f"System time: {time.perf_counter():.6f}") -data_stream = device.send(HarpMessage.ReadU8(33).frame) # returns a ReplyHarpMessage -# data_stream = device.send(HarpMessage.ReadS16(33).frame).payload_as_int_array() +data_stream = device.read_u8(33) + print(f"Data Stream payload type: {data_stream.payload_type.name}") print(f"Data Stream message type: {data_stream.message_type.name}") print(f"Data Stream timestamp: {data_stream.timestamp}") @@ -34,9 +33,7 @@ print(f"Data Stream payload: {data_stream.payload}") print(f"System time: {time.perf_counter():.6f}") -event_reg_response = device.send( - HarpMessage.ReadU8(77).frame -) # returns a ReplyHarpMessage +event_reg_response = device.read_u8(77) # returns a ReplyHarpMessage print(f"EVNT_ENABLE payload type: {event_reg_response.payload_type.name}") print(f"EVNT_ENABLE message type: {event_reg_response.message_type.name}") print(f"EVNT_ENABLE timestamp: {event_reg_response.timestamp}") diff --git a/pyharp/device.py b/pyharp/device.py index fa06a26..f782086 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -8,10 +8,10 @@ import serial -from pyharp.base import CommonRegisters +from pyharp.base import CommonRegisters, PayloadType from pyharp.device_names import device_names from pyharp.harp_serial import HarpSerial -from pyharp.messages import HarpMessage, ReplyHarpMessage +from pyharp.messages import HarpMessage, ReadHarpMessage, ReplyHarpMessage class DeviceMode(Enum): @@ -100,106 +100,76 @@ def disconnect(self) -> None: def read_who_am_i(self) -> int: address = CommonRegisters.WHO_AM_I - reply: ReplyHarpMessage = self.send( - HarpMessage.ReadU16(address).frame, dump=False - ) - #print(str(reply)) + reply: ReplyHarpMessage = self.read_u16(address, dump=False) + return reply.payload_as_int() def read_who_am_i_device(self) -> str: address = CommonRegisters.WHO_AM_I - reply: ReplyHarpMessage = self.send( - HarpMessage.ReadU16(address).frame, dump=False - ) - #print(str(reply)) + reply: ReplyHarpMessage = self.read_u16(address, dump=False) return device_names.get(reply.payload_as_int()) def read_hw_version_h(self) -> int: address = CommonRegisters.HW_VERSION_H - reply: ReplyHarpMessage = self.send( - HarpMessage.ReadU8(address).frame, dump=False - ) - #print(str(reply)) + reply: ReplyHarpMessage = self.read_u8(address, dump=False) return reply.payload_as_int() def read_hw_version_l(self) -> int: address = CommonRegisters.HW_VERSION_L - reply: ReplyHarpMessage = self.send( - HarpMessage.ReadU8(address).frame, dump=False - ) - #print(str(reply)) + reply: ReplyHarpMessage = self.read_u8(address, dump=False) return reply.payload_as_int() def read_assembly_version(self) -> int: address = CommonRegisters.ASSEMBLY_VERSION - reply: ReplyHarpMessage = self.send( - HarpMessage.ReadU8(address).frame, dump=False - ) - #print(str(reply)) + reply: ReplyHarpMessage = self.read_u8(address, dump=False) return reply.payload_as_int() def read_harp_h_version(self) -> int: address = CommonRegisters.HARP_VERSION_H - reply: ReplyHarpMessage = self.send( - HarpMessage.ReadU8(address).frame, dump=False - ) - #print(str(reply)) + reply: ReplyHarpMessage = self.read_u8(address, dump=False) return reply.payload_as_int() def read_harp_l_version(self) -> int: address = CommonRegisters.HARP_VERSION_L - reply: ReplyHarpMessage = self.send( - HarpMessage.ReadU8(address).frame, dump=False - ) - #print(str(reply)) + reply: ReplyHarpMessage = self.read_u8(address, dump=False) return reply.payload_as_int() def read_fw_h_version(self) -> int: address = CommonRegisters.FIRMWARE_VERSION_H - reply: ReplyHarpMessage = self.send( - HarpMessage.ReadU8(address).frame, dump=False - ) - #print(str(reply)) + reply: ReplyHarpMessage = self.read_u8(address, dump=False) return reply.payload_as_int() def read_fw_l_version(self) -> int: address = CommonRegisters.FIRMWARE_VERSION_L - reply: ReplyHarpMessage = self.send( - HarpMessage.ReadU8(address).frame, dump=False -) - #print(str(reply)) + reply: ReplyHarpMessage = self.read_u8(address, dump=False) return reply.payload_as_int() def read_device_name(self) -> str: address = CommonRegisters.DEVICE_NAME - # reply: Optional[bytes] = self.send(HarpMessage.ReadU8(address).frame, 13 + 24) - reply: ReplyHarpMessage = self.send( - HarpMessage.ReadU8(address).frame, dump=False - ) - #print(str(reply)) + reply: ReplyHarpMessage = self.read_u8(address, dump=False) return reply.payload_as_string() def read_device_mode(self) -> DeviceMode: address = CommonRegisters.OPERATION_CTRL - reply = self.send(HarpMessage.ReadU8(address).frame) + reply = self.read_u8(address) return DeviceMode(reply.payload_as_int() & 0x03) def dump_registers(self) -> list: @@ -207,7 +177,7 @@ def dump_registers(self) -> list: as Harp Read Reply Messages. """ address = CommonRegisters.OPERATION_CTRL - reg_value = self.send(HarpMessage.ReadU8(address).frame).payload_as_int() + reg_value = self.read_u8(address).payload_as_int() reg_value |= 0x08 # Assert DUMP bit self._ser.write(HarpMessage.WriteU8(address, reg_value).frame) replies = [] @@ -224,7 +194,7 @@ def set_mode(self, mode: DeviceMode) -> ReplyHarpMessage: """Change the device's OPMODE. Reply can be ignored.""" address = CommonRegisters.OPERATION_CTRL # Read register first. - reg_value = self.send(HarpMessage.ReadU8(address).frame).payload_as_int() + reg_value = self.read_u8(address).payload_as_int() reg_value &= ~0x03 # mask off old mode. reg_value |= mode.value reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) @@ -234,7 +204,7 @@ def enable_status_led(self): """enable the device's status led if one exists.""" address = CommonRegisters.OPERATION_CTRL # Read register first. - reg_value = self.send(HarpMessage.ReadU8(address).frame).payload_as_int() + reg_value = self.read_u8(address).payload_as_int() reg_value |= (1 << 5) reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) @@ -242,7 +212,7 @@ def disable_status_led(self): """disable the device's status led if one exists.""" address = CommonRegisters.OPERATION_CTRL # Read register first. - reg_value = self.send(HarpMessage.ReadU8(address).frame).payload_as_int() + reg_value = self.read_u8(address).payload_as_int() reg_value &= ~(1 << 5) reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) @@ -250,7 +220,7 @@ def enable_alive_en(self): """Enable ALIVE_EN such that the device sends an event each second.""" address = CommonRegisters.OPERATION_CTRL # Read register first. - reg_value = self.send(HarpMessage.ReadU8(address).frame).payload_as_int() + reg_value = self.read_u8(address).payload_as_int() reg_value |= (1 << 7) reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) @@ -258,7 +228,7 @@ def disable_alive_en(self): """disable ALIVE_EN such that the device does not send an event each second.""" address = CommonRegisters.OPERATION_CTRL # Read register first. - reg_value = self.send(HarpMessage.ReadU8(address).frame).payload[0] + reg_value = self.read_u8(address).payload[0] reg_value &= ((1<< 7) ^ 0xFF) # bitwise ~ operator substitute for Python ints. reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) @@ -306,4 +276,39 @@ def get_events(self) -> list[ReplyHarpMessage]: def event_count(self) -> int: """Get the number of events in the event queue.""" - return self._ser.event_q.qsize() \ No newline at end of file + return self._ser.event_q.qsize() + + def read_u8(self, address: int, dump: bool = True) -> ReplyHarpMessage: + return self.send( + ReadHarpMessage(payload_type=PayloadType.U8, address=address).frame, dump + ) + + def read_s8(self, address: int, dump: bool = True) -> ReplyHarpMessage: + return self.send( + ReadHarpMessage(payload_type=PayloadType.S8, address=address).frame, dump + ) + + def read_u16(self, address: int, dump: bool = True) -> ReplyHarpMessage: + return self.send( + ReadHarpMessage(payload_type=PayloadType.U16, address=address).frame, dump + ) + + def read_s16(self, address: int, dump: bool = True) -> ReplyHarpMessage: + return self.send( + ReadHarpMessage(payload_type=PayloadType.S16, address=address).frame, dump + ) + + def read_u32(self, address: int, dump: bool = True) -> ReplyHarpMessage: + return self.send( + ReadHarpMessage(payload_type=PayloadType.U32, address=address).frame, dump + ) + + def read_s32(self, address: int, dump: bool = True) -> ReplyHarpMessage: + return self.send( + ReadHarpMessage(payload_type=PayloadType.S32, address=address).frame, dump + ) + + def read_float(self, address: int, dump: bool = True) -> ReplyHarpMessage: + return self.send( + ReadHarpMessage(payload_type=PayloadType.FLOAT, address=address).frame, dump + ) diff --git a/pyharp/drivers/behavior.py b/pyharp/drivers/behavior.py index 989f89a..c36dd07 100644 --- a/pyharp/drivers/behavior.py +++ b/pyharp/drivers/behavior.py @@ -1,12 +1,14 @@ """Behavior Device Driver.""" -from pyharp.messages import HarpMessage, ReplyHarpMessage -from pyharp.device_names import device_names -from pyharp.device import Device +from enum import Enum + import serial from serial.serialutil import SerialException -from enum import Enum +from pyharp.base import PayloadType +from pyharp.device import Device +from pyharp.device_names import device_names +from pyharp.messages import HarpMessage, ReadHarpMessage, ReplyHarpMessage # These definitions are from app_regs.h in the firmware. # Type, Base Address, "Description." @@ -84,11 +86,10 @@ class Behavior: ID = 1216 # TODO: put this in a base class? - READ_MSG_LOOKUP = \ - { - "U8": HarpMessage.ReadU8, - "U16": HarpMessage.ReadU16, - "S16": HarpMessage.ReadS16, + READ_MSG_LOOKUP = { + "U8": PayloadType.U8, + "U16": PayloadType.U16, + "S16": PayloadType.S16, } WRITE_MSG_LOOKUP = \ @@ -155,7 +156,9 @@ def all_input_states(self): """return the state of all PORT digital inputs.""" reg_type, reg_index, _ = REGISTERS["PORT_DIS"] read_message_type = self.__class__.READ_MSG_LOOKUP[reg_type] - return self.device.send(read_message_type(reg_index).frame).payload_as_int() + return self.device.send( + ReadHarpMessage(read_message_type, reg_index).frame + ).payload_as_int() @property def DI0(self): @@ -248,7 +251,9 @@ def all_output_states(self): """return the state of all PORT digital inputs.""" reg_type, reg_index, _ = REGISTERS["OUTPUTS_OUT"] read_message_type = self.__class__.READ_MSG_LOOKUP[reg_type] - return self.device.send(read_message_type(reg_index).frame).payload_as_int() + return self.device.send( + ReadHarpMessage(read_message_type, reg_index).frame + ).payload_as_int() @all_output_states.setter def all_output_states(self, bitmask : int): diff --git a/tests/test_device.py b/tests/test_device.py index cc5dff2..31306ae 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -23,8 +23,7 @@ def test_read_U8() -> None: register: int = 38 read_size: int = 35 # TODO: automatically calculate this! - read_message = HarpMessage.ReadU8(register) - reply: ReplyHarpMessage = device.send(read_message.frame) + reply: ReplyHarpMessage = device.read_u8(register) assert reply is not None # assert reply.payload_as_int() == write_value @@ -50,8 +49,7 @@ def test_U8() -> None: assert reply is not None # read register 38 - read_message = HarpMessage.ReadU8(register) - reply = device.send(read_message.frame) + reply = device.read_u8(register) assert reply is not None assert reply.payload_as_int() == write_value diff --git a/tests/test_messages.py b/tests/test_messages.py index a5e1ae3..13fc637 100644 --- a/tests/test_messages.py +++ b/tests/test_messages.py @@ -1,11 +1,11 @@ -from pyharp.base import CommonRegisters -from pyharp.messages import HarpMessage, MessageType +from pyharp.base import CommonRegisters, PayloadType +from pyharp.messages import HarpMessage, MessageType, ReadHarpMessage DEFAULT_ADDRESS = 42 def test_create_read_U8() -> None: - message = HarpMessage.ReadU8(DEFAULT_ADDRESS) + message = ReadHarpMessage(payload_type=PayloadType.U8, address=DEFAULT_ADDRESS) assert message.message_type == MessageType.READ assert message.checksum == 47 # 1 + 4 + 42 + 255 + 1 - 256 @@ -13,7 +13,7 @@ def test_create_read_U8() -> None: def test_create_read_S8() -> None: - message = HarpMessage.ReadS8(DEFAULT_ADDRESS) + message = ReadHarpMessage(payload_type=PayloadType.S8, address=DEFAULT_ADDRESS) assert message.message_type == MessageType.READ assert message.checksum == 175 # 1 + 4 + 42 + 255 + 129 - 256 @@ -21,7 +21,7 @@ def test_create_read_S8() -> None: def test_create_read_U16() -> None: - message = HarpMessage.ReadU16(DEFAULT_ADDRESS) + message = ReadHarpMessage(payload_type=PayloadType.U16, address=DEFAULT_ADDRESS) assert message.message_type == MessageType.READ assert message.checksum == 48 # 1 + 4 + 42 + 255 + 2 - 256 @@ -29,7 +29,7 @@ def test_create_read_U16() -> None: def test_create_read_S16() -> None: - message = HarpMessage.ReadS16(DEFAULT_ADDRESS) + message = ReadHarpMessage(payload_type=PayloadType.S16, address=DEFAULT_ADDRESS) assert message.message_type == MessageType.READ assert message.checksum == 176 # 1 + 4 + 42 + 255 + 130 - 256 @@ -79,6 +79,8 @@ def test_create_write_S16() -> None: def test_read_who_am_i() -> None: - message = HarpMessage.ReadU16(CommonRegisters.WHO_AM_I) + message = ReadHarpMessage( + payload_type=PayloadType.U16, address=CommonRegisters.WHO_AM_I + ) assert str(message.frame) == str(bytearray(b"\x01\x04\x00\xff\x02\x06")) From bcfcf3c4cf7241a91f33d0f2de5a602b89679925 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Tue, 25 Mar 2025 11:31:08 +0000 Subject: [PATCH 049/267] Remove ReadXX methods from HarpMessage --- pyharp/messages.py | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/pyharp/messages.py b/pyharp/messages.py index d8f683b..d1ba6c6 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -51,34 +51,6 @@ def payload_type(self) -> PayloadType: def checksum(self) -> int: return self._frame[-1] - @staticmethod - def ReadU8(address: int) -> ReadHarpMessage: - return ReadHarpMessage(payload_type=PayloadType.U8, address=address) - - @staticmethod - def ReadS8(address: int) -> ReadHarpMessage: - return ReadHarpMessage(payload_type=PayloadType.S8, address=address) - - @staticmethod - def ReadS16(address: int) -> ReadHarpMessage: - return ReadHarpMessage(payload_type=PayloadType.S16, address=address) - - @staticmethod - def ReadU16(address: int) -> ReadHarpMessage: - return ReadHarpMessage(payload_type=PayloadType.U16, address=address) - - @staticmethod - def ReadU32(address: int) -> ReadHarpMessage: - return ReadHarpMessage(payload_type=PayloadType.U32, address=address) - - @staticmethod - def ReadS32(address: int) -> ReadHarpMessage: - return ReadHarpMessage(payload_type=PayloadType.S32, address=address) - - @staticmethod - def ReadFloat(address: int) -> ReadHarpMessage: - return ReadHarpMessage(payload_type=PayloadType.Float, address=address) - @staticmethod def WriteU8(address: int, value: int) -> WriteU8HarpMessage: return WriteU8HarpMessage(address, value) From f4745038e41db3743dab5ea73be024fa21f2e267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Tue, 25 Mar 2025 16:06:52 +0000 Subject: [PATCH 050/267] Extract unrelated Enum members --- pyharp/base.py | 48 +++++++++++++++++++++++----------------------- pyharp/messages.py | 2 +- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/pyharp/base.py b/pyharp/base.py index 70daabd..271a903 100644 --- a/pyharp/base.py +++ b/pyharp/base.py @@ -1,5 +1,10 @@ from enum import Enum +# TODO: Find a way to really hide this from the user +_isUnsigned: int = 0x00 +_isSigned: int = 0x80 +_isFloat: int = 0x40 +_hasTimestamp: int = 0x10 class MessageType(Enum): READ: int = 1 @@ -10,30 +15,25 @@ class MessageType(Enum): class PayloadType(Enum): - isUnsigned: int = 0x00 - isSigned: int = 0x80 - isFloat: int = 0x40 - hasTimestamp: int = 0x10 - - U8 = isUnsigned | 1 # 1 - S8 = isSigned | 1 # 129 - U16 = isUnsigned | 2 # 2 - S16 = isSigned | 2 # 130 - U32 = isUnsigned | 4 - S32 = isSigned | 4 - U64 = isUnsigned | 8 - S64 = isSigned | 8 - Float = isFloat | 4 - Timestamp = hasTimestamp - TimestampedU8 = hasTimestamp | U8 - TimestampedS8 = hasTimestamp | S8 - TimestampedU16 = hasTimestamp | U16 - TimestampedS16 = hasTimestamp | S16 - TimestampedU32 = hasTimestamp | U32 - TimestampedS32 = hasTimestamp | S32 - TimestampedU64 = hasTimestamp | U64 - TimestampedS64 = hasTimestamp | S64 - TimestampedFloat = hasTimestamp | Float + U8 = _isUnsigned | 1 # 1 + S8 = _isSigned | 1 # 129 + U16 = _isUnsigned | 2 # 2 + S16 = _isSigned | 2 # 130 + U32 = _isUnsigned | 4 + S32 = _isSigned | 4 + U64 = _isUnsigned | 8 + S64 = _isSigned | 8 + Float = _isFloat | 4 + Timestamp = _hasTimestamp + TimestampedU8 = _hasTimestamp | U8 + TimestampedS8 = _hasTimestamp | S8 + TimestampedU16 = _hasTimestamp | U16 + TimestampedS16 = _hasTimestamp | S16 + TimestampedU32 = _hasTimestamp | U32 + TimestampedS32 = _hasTimestamp | S32 + TimestampedU64 = _hasTimestamp | U64 + TimestampedS64 = _hasTimestamp | S64 + TimestampedFloat = _hasTimestamp | Float class CommonRegisters: diff --git a/pyharp/messages.py b/pyharp/messages.py index d1ba6c6..30aac30 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -108,7 +108,7 @@ def __init__( + int.from_bytes(frame[9:11], byteorder="little", signed=False) * 32e-6 ) # Timestamp is junk if it's not present. - if not (self.payload_type.value & PayloadType.hasTimestamp.value): + if not (self.payload_type.value & PayloadType.Timestamp.value): self._timestamp = None def _parse_payload(self, raw_payload) -> list[int]: From ab0a7da1a5c13d9f530bd62b05f6a35f0cf8f0ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Tue, 25 Mar 2025 16:08:45 +0000 Subject: [PATCH 051/267] Add read_S64 and read_U64 methods to Device --- pyharp/device.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pyharp/device.py b/pyharp/device.py index f782086..8711fdc 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -308,6 +308,16 @@ def read_s32(self, address: int, dump: bool = True) -> ReplyHarpMessage: ReadHarpMessage(payload_type=PayloadType.S32, address=address).frame, dump ) + def read_u64(self, address: int, dump: bool = True) -> ReplyHarpMessage: + return self.send( + ReadHarpMessage(payload_type=PayloadType.U64, address=address).frame, dump + ) + + def read_s64(self, address: int, dump: bool = True) -> ReplyHarpMessage: + return self.send( + ReadHarpMessage(payload_type=PayloadType.S64, address=address).frame, dump + ) + def read_float(self, address: int, dump: bool = True) -> ReplyHarpMessage: return self.send( ReadHarpMessage(payload_type=PayloadType.FLOAT, address=address).frame, dump From 0859612c113e436d6e25da3d7f5038666dde4ff4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Tue, 25 Mar 2025 16:12:56 +0000 Subject: [PATCH 052/267] Create write_XXX methods on Device --- pyharp/device.py | 104 ++++++++++++++++-- pyharp/drivers/behavior.py | 113 +++++++++---------- pyharp/messages.py | 219 +++++++++++++++---------------------- tests/test_device.py | 9 +- tests/test_messages.py | 18 +-- 5 files changed, 242 insertions(+), 221 deletions(-) diff --git a/pyharp/device.py b/pyharp/device.py index 8711fdc..f4f160a 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -4,14 +4,14 @@ import queue from enum import Enum from pathlib import Path -from typing import Optional, Union +from typing import List, Optional, Union import serial from pyharp.base import CommonRegisters, PayloadType from pyharp.device_names import device_names from pyharp.harp_serial import HarpSerial -from pyharp.messages import HarpMessage, ReadHarpMessage, ReplyHarpMessage +from pyharp.messages import ReadHarpMessage, ReplyHarpMessage, WriteHarpMessage class DeviceMode(Enum): @@ -179,7 +179,7 @@ def dump_registers(self) -> list: address = CommonRegisters.OPERATION_CTRL reg_value = self.read_u8(address).payload_as_int() reg_value |= 0x08 # Assert DUMP bit - self._ser.write(HarpMessage.WriteU8(address, reg_value).frame) + self._ser.write(WriteHarpMessage(PayloadType.U8, address, reg_value).frame) replies = [] while True: msg = self._read() @@ -197,7 +197,7 @@ def set_mode(self, mode: DeviceMode) -> ReplyHarpMessage: reg_value = self.read_u8(address).payload_as_int() reg_value &= ~0x03 # mask off old mode. reg_value |= mode.value - reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) + reply = self.send(WriteHarpMessage(PayloadType.U8, address, reg_value).frame) return reply def enable_status_led(self): @@ -206,7 +206,7 @@ def enable_status_led(self): # Read register first. reg_value = self.read_u8(address).payload_as_int() reg_value |= (1 << 5) - reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) + reply = self.send(WriteHarpMessage(PayloadType.U8, address, reg_value).frame) def disable_status_led(self): """disable the device's status led if one exists.""" @@ -214,7 +214,7 @@ def disable_status_led(self): # Read register first. reg_value = self.read_u8(address).payload_as_int() reg_value &= ~(1 << 5) - reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) + reply = self.send(WriteHarpMessage(PayloadType.U8, address, reg_value).frame) def enable_alive_en(self): """Enable ALIVE_EN such that the device sends an event each second.""" @@ -222,22 +222,21 @@ def enable_alive_en(self): # Read register first. reg_value = self.read_u8(address).payload_as_int() reg_value |= (1 << 7) - reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) + reply = self.send(WriteHarpMessage(PayloadType.U8, address, reg_value).frame) def disable_alive_en(self): """disable ALIVE_EN such that the device does not send an event each second.""" address = CommonRegisters.OPERATION_CTRL # Read register first. reg_value = self.read_u8(address).payload[0] - reg_value &= ((1<< 7) ^ 0xFF) # bitwise ~ operator substitute for Python ints. - reply = self.send(HarpMessage.WriteU8(address, reg_value).frame) + reg_value &= (1 << 7) ^ 0xFF # bitwise ~ operator substitute for Python ints. + reply = self.send(WriteHarpMessage(PayloadType.U8, address, reg_value).frame) def reset_device(self): address = CommonRegisters.RESET_DEV # reset_value = 0xFF & (1< ReplyHarpMessage: """Send a harp message; return the device's reply.""" @@ -320,5 +319,86 @@ def read_s64(self, address: int, dump: bool = True) -> ReplyHarpMessage: def read_float(self, address: int, dump: bool = True) -> ReplyHarpMessage: return self.send( - ReadHarpMessage(payload_type=PayloadType.FLOAT, address=address).frame, dump + ReadHarpMessage(payload_type=PayloadType.Float, address=address).frame, dump ) + + def write_u8(self, address: int, value: int | List[int]) -> ReplyHarpMessage: + return self.send( + WriteHarpMessage( + payload_type=PayloadType.U8, + address=address, + value=value, + ).frame + ) + + def write_s8(self, address: int, value: int | List[int]) -> ReplyHarpMessage: + return self.send( + WriteHarpMessage( + payload_type=PayloadType.S8, + address=address, + value=value, + ).frame + ) + + def write_u16(self, address: int, value: int | List[int]) -> ReplyHarpMessage: + return self.send( + WriteHarpMessage( + payload_type=PayloadType.U16, + address=address, + value=value, + ).frame + ) + + def write_s16(self, address: int, value: int | List[int]) -> ReplyHarpMessage: + return self.send( + WriteHarpMessage( + payload_type=PayloadType.S16, + address=address, + value=value, + ).frame + ) + + def write_u32(self, address: int, value: int | List[int]) -> ReplyHarpMessage: + return self.send( + WriteHarpMessage( + payload_type=PayloadType.U32, + address=address, + value=value, + ).frame + ) + + def write_s32(self, address: int, value: int | List[int]) -> ReplyHarpMessage: + return self.send( + WriteHarpMessage( + payload_type=PayloadType.S32, + address=address, + value=value, + ).frame + ) + + def write_u64(self, address: int, value: int | List[int]) -> ReplyHarpMessage: + return self.send( + WriteHarpMessage( + payload_type=PayloadType.U64, + address=address, + value=value, + ).frame + ) + + def write_s64(self, address: int, value: int | List[int]) -> ReplyHarpMessage: + return self.send( + WriteHarpMessage( + payload_type=PayloadType.S64, + address=address, + value=value, + ).frame + ) + + def write_float(self, address: int, value: float | List[float]) -> ReplyHarpMessage: + return self.send( + WriteHarpMessage( + payload_type=PayloadType.Float, + address=address, + value=value, + ).frame + ) \ No newline at end of file diff --git a/pyharp/drivers/behavior.py b/pyharp/drivers/behavior.py index c36dd07..2f4b333 100644 --- a/pyharp/drivers/behavior.py +++ b/pyharp/drivers/behavior.py @@ -2,37 +2,38 @@ from enum import Enum -import serial from serial.serialutil import SerialException from pyharp.base import PayloadType from pyharp.device import Device -from pyharp.device_names import device_names -from pyharp.messages import HarpMessage, ReadHarpMessage, ReplyHarpMessage +from pyharp.messages import ReadHarpMessage, ReplyHarpMessage, WriteHarpMessage # These definitions are from app_regs.h in the firmware. # Type, Base Address, "Description." -REGISTERS = \ -{ # RJ45 "PORT" (0, 1, 2) Digital Inputs - "PORT_DIS" : ("U8", 32, "Reflects the state of DI digital lines of each Port."), - +REGISTERS = { # RJ45 "PORT" (0, 1, 2) Digital Inputs + "PORT_DIS": ( + PayloadType.U8, + 32, + "Reflects the state of DI digital lines of each Port.", + ), # Manipulate any of the board's digital outputs. - "OUTPUTS_SET": ("U16", 34, "Set the corresponding output."), - "OUTPUTS_CLR": ("U16", 35, "Clear the corresponding output."), - "OUTPUTS_TOGGLE": ("U16", 36, "Toggle the corresponding output."), - "OUTPUTS_OUT": ("U16", 37, "Control corresponding output."), - + "OUTPUTS_SET": (PayloadType.U16, 34, "Set the corresponding output."), + "OUTPUTS_CLR": (PayloadType.U16, 35, "Clear the corresponding output."), + "OUTPUTS_TOGGLE": (PayloadType.U16, 36, "Toggle the corresponding output."), + "OUTPUTS_OUT": (PayloadType.U16, 37, "Control corresponding output."), # RJ45 "PORT" (0, 1, 2) Digital IOs - "PORT_DIOS_SET": ("U8", 38, "Set the corresponding DIO."), - "PORT_DIOS_CLEAR": ("U8", 39, "Clear the corresponding DIO."), - "PORT_DIOS_TOGGLE": ("U8", 40, "Toggle the corresponding DIO."), - "PORT_DIOS_OUT": ("U8", 41, "Control the corresponding DIO."), - "PORT_DIOS_CONF": ("U8", 42, "Set the DIOs direction (1 is output)."), - "PORT_DIOS_IN": ("U8", 43, "State of the DIOs."), - - "ADD_REG_DATA": ("S16", 44, "Voltage at ADC input and decoder (poke2) value."), - - "EVNT_ENABLE": ("U8", 77, "Enable events within the bitfields."), + "PORT_DIOS_SET": (PayloadType.U8, 38, "Set the corresponding DIO."), + "PORT_DIOS_CLEAR": (PayloadType.U8, 39, "Clear the corresponding DIO."), + "PORT_DIOS_TOGGLE": (PayloadType.U8, 40, "Toggle the corresponding DIO."), + "PORT_DIOS_OUT": (PayloadType.U8, 41, "Control the corresponding DIO."), + "PORT_DIOS_CONF": (PayloadType.U8, 42, "Set the DIOs direction (1 is output)."), + "PORT_DIOS_IN": (PayloadType.U8, 43, "State of the DIOs."), + "ADD_REG_DATA": ( + PayloadType.S16, + 44, + "Voltage at ADC input and decoder (poke2) value.", + ), + "EVNT_ENABLE": (PayloadType.U8, 77, "Enable events within the bitfields."), } @@ -42,6 +43,7 @@ class PORT_DIS(Enum): DI1 = 1 DI2 = 2 + class OUTPUTS_OUT(Enum): PORT0_DO = 0 PORT0_D1 = 1 @@ -61,6 +63,7 @@ class OUTPUTS_OUT(Enum): DO2 = 12 DO3 = 13 + class PORT_DIOS_IN(Enum): DIO0 = 0 DIO1 = 0 @@ -69,11 +72,11 @@ class PORT_DIOS_IN(Enum): # reader-friendly events for enabling/disabling. class Events(Enum): - port_digital_inputs = 0 # PORT_DIS - port_digital_ios = 1 # PORT_DIOS_IN - analog_input = 2 # DATA - cam0 = 3 # CAM0 - cam1 = 3 # CAM1 + port_digital_inputs = 0 # PORT_DIS + port_digital_ios = 1 # PORT_DIOS_IN + analog_input = 2 # DATA + cam0 = 3 # CAM0 + cam1 = 3 # CAM1 class Behavior: @@ -85,21 +88,6 @@ class Behavior: DEFAULT_PORT_NAME = "/dev/harp_device_00" ID = 1216 - # TODO: put this in a base class? - READ_MSG_LOOKUP = { - "U8": PayloadType.U8, - "U16": PayloadType.U16, - "S16": PayloadType.S16, - } - - WRITE_MSG_LOOKUP = \ - { - "U8": HarpMessage.WriteU8, - "U16": HarpMessage.WriteU16, - "S16": HarpMessage.WriteS16, - } - - def __init__(self, port_name=None, output_filename=None): """Class constructor. Connect to a device.""" @@ -135,8 +123,9 @@ def disable_all_events(self) -> ReplyHarpMessage: (1 << Events.cam0.value) | \ (1 << Events.cam1.value) ) ^ 0xFF) reg_type, reg_index, _ = REGISTERS["EVNT_ENABLE"] - write_message_type = self.__class__.WRITE_MSG_LOOKUP[reg_type] - return self.device.send(write_message_type(reg_index, event_reg_bitmask).frame) + return self.device.send( + WriteHarpMessage(reg_type, reg_index, event_reg_bitmask).frame + ) def enable_events(self, *events: Events) -> ReplyHarpMessage: @@ -145,8 +134,9 @@ def enable_events(self, *events: Events) -> ReplyHarpMessage: for event in events: event_reg_bitmask |= (1 << event.value) reg_type, reg_index, _ = REGISTERS["EVNT_ENABLE"] - write_message_type = self.__class__.WRITE_MSG_LOOKUP[reg_type] - return self.device.send(write_message_type(reg_index, event_reg_bitmask).frame) + return self.device.send( + WriteHarpMessage(reg_type, reg_index, event_reg_bitmask).frame + ) # Board inputs, outputs, and some settings configured as @properties. @@ -155,9 +145,8 @@ def enable_events(self, *events: Events) -> ReplyHarpMessage: def all_input_states(self): """return the state of all PORT digital inputs.""" reg_type, reg_index, _ = REGISTERS["PORT_DIS"] - read_message_type = self.__class__.READ_MSG_LOOKUP[reg_type] return self.device.send( - ReadHarpMessage(read_message_type, reg_index).frame + ReadHarpMessage(reg_type, reg_index).frame ).payload_as_int() @property @@ -183,8 +172,8 @@ def DI2(self): # def set_io_configuration(self, bitmask : int): # """set the state of all PORT digital ios. (1 is output.)""" # reg_type, reg_index, _ = REGISTERS["PORT_DIOS_CONF"] -# write_message_type = self.__class__.WRITE_MSG_LOOKUP[reg_type] -# self.device.send(write_message_type(reg_index, bitmask).frame) + + # self.device.send(WriteHarpMessage(reg_type, reg_index, bitmask).frame) # # @property # def all_io_states(self): @@ -199,20 +188,20 @@ def DI2(self): # # Setting the state of the "DIO" pins, requires writing to the # # _IN register, which is different from the OUTPUT # reg_type, reg_index, _ = REGISTERS["PORT_DIOS_IN"] -# write_message_type = self.__class__.WRITE_MSG_LOOKUP[reg_type] -# return self.device.send(write_message_type(reg_index, bitmask).frame) + + # return self.device.send(WriteHarpMessage(reg_type, reg_index, bitmask).frame) # # def set_io_outputs(self, bitmask : int): # """set digital input/outputs to logic 1 according to bitmask.""" # reg_type, reg_index, _ = REGISTERS["PORT_DIOS_SET"] -# write_message_type = self.__class__.WRITE_MSG_LOOKUP[reg_type] -# return self.device.send(write_message_type(reg_index, bitmask).frame) + + # return self.device.send(WriteHarpMessage(reg_type, reg_index, bitmask).frame) # # def clear_io_outputs(self, bitmask : int): # """clear digital input/outputs (specified with logic 1) according to bitmask.""" # reg_type, reg_index, _ = REGISTERS["PORT_DIOS_CLEAR"] -# write_message_type = self.__class__.WRITE_MSG_LOOKUP[reg_type] -# return self.device.send(write_message_type(reg_index, bitmask).frame) + + # return self.device.send(WriteHarpMessage(reg_type, reg_index, bitmask).frame) # # @property # def port0_io0(self): @@ -250,29 +239,25 @@ def DI2(self): def all_output_states(self): """return the state of all PORT digital inputs.""" reg_type, reg_index, _ = REGISTERS["OUTPUTS_OUT"] - read_message_type = self.__class__.READ_MSG_LOOKUP[reg_type] return self.device.send( - ReadHarpMessage(read_message_type, reg_index).frame + ReadHarpMessage(reg_type, reg_index).frame ).payload_as_int() @all_output_states.setter def all_output_states(self, bitmask : int): """set the state of all PORT digital inputs.""" reg_type, reg_index, _ = REGISTERS["OUTPUTS_OUT"] - write_message_type = self.__class__.WRITE_MSG_LOOKUP[reg_type] - return self.device.send(write_message_type(reg_index, bitmask).frame) + return self.device.send(WriteHarpMessage(reg_type, reg_index, bitmask).frame) def set_outputs(self, bitmask : int): """set digital outputs to logic 1 according to bitmask.""" reg_type, reg_index, _ = REGISTERS["OUTPUTS_SET"] - write_message_type = self.__class__.WRITE_MSG_LOOKUP[reg_type] - return self.device.send(write_message_type(reg_index, bitmask).frame) + return self.device.send(WriteHarpMessage(reg_type, reg_index, bitmask).frame) def clear_outputs(self, bitmask : int): """clear digital outputs (specified with logic 1) according to bitmask.""" reg_type, reg_index, _ = REGISTERS["OUTPUTS_CLR"] - write_message_type = self.__class__.WRITE_MSG_LOOKUP[reg_type] - return self.device.send(write_message_type(reg_index, bitmask).frame) + return self.device.send(WriteHarpMessage(reg_type, reg_index, bitmask).frame) @property def D0(self): diff --git a/pyharp/messages.py b/pyharp/messages.py index 30aac30..13b7d01 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -51,34 +51,6 @@ def payload_type(self) -> PayloadType: def checksum(self) -> int: return self._frame[-1] - @staticmethod - def WriteU8(address: int, value: int) -> WriteU8HarpMessage: - return WriteU8HarpMessage(address, value) - - @staticmethod - def WriteS8(address: int, value: int) -> WriteS8HarpMessage: - return WriteS8HarpMessage(address, value) - - @staticmethod - def WriteS16(address: int, value: int) -> WriteS16HarpMessage: - return WriteS16HarpMessage(address, value) - - @staticmethod - def WriteU16(address: int, value: int) -> WriteU16HarpMessage: - return WriteU16HarpMessage(address, value) - - @staticmethod - def WriteFloat(address: int, value: int) -> WriteFloatHarpMessage: - return WriteFloatHarpMessage(address, value) - - @staticmethod - def WriteU32(address: int, value: int) -> WriteU32HarpMessage: - return WriteU32HarpMessage(address, value) - - @staticmethod - def WriteS32(address: int, value: int) -> WriteS32HarpMessage: - return WriteS32HarpMessage(address, value) - @staticmethod def parse(frame: bytearray) -> ReplyHarpMessage: return ReplyHarpMessage(frame) @@ -86,6 +58,10 @@ def parse(frame: bytearray) -> ReplyHarpMessage: # A Response Message from a harp device. class ReplyHarpMessage(HarpMessage): + """ + A Response Message from a harp device. + """ + def __init__( self, frame: bytearray, @@ -197,122 +173,103 @@ def __init__(self, payload_type: PayloadType, address: int): class WriteHarpMessage(HarpMessage): BASE_LENGTH: int = 5 + BASE_LENGTH: int = 4 MESSAGE_TYPE: int = MessageType.WRITE + # Define payload type properties + _PAYLOAD_CONFIG = { + # payload_type: (byte_size, signed, is_float) + PayloadType.U8: (1, False, False), + PayloadType.S8: (1, True, False), + PayloadType.U16: (2, False, False), + PayloadType.S16: (2, True, False), + PayloadType.U32: (4, False, False), + PayloadType.S32: (4, True, False), + PayloadType.U64: (8, False, False), + PayloadType.S64: (8, True, False), + PayloadType.Float: (4, False, True), + } + def __init__( - self, payload_type: PayloadType, payload: bytes, address: int, offset: int = 0 + self, + payload_type: PayloadType, + address: int, + value: int | float | List[int] | List[float] = None, ): """ - - :param payload_type: - :param payload: - :param address: - :param offset: how many bytes more besides the length corresponding to U8 (for example, for U16 it would be offset=1) + Create a WriteHarpMessage to send to a device. + + Parameters + ---------- + payload_type : PayloadType + Type of payload (U8, S8, U16, etc.) + address : int + Register address to write to + value : int, float, List[int], or List[float], optional + Value(s) to write - can be a single value or list of values + + Notes + ----- + The message frame is constructed according to the HARP binary protocol. + The length is calculated as BASE_LENGTH + payload size in bytes. """ + self._frame = bytearray() - self._frame.append(self.MESSAGE_TYPE.value) + # Get configuration for this payload type + byte_size, signed, is_float = self._PAYLOAD_CONFIG.get( + payload_type, (1, False, False) + ) + + # Convert value to payload bytes + payload = bytearray() + values = value if isinstance(value, list) else [value] - self._frame.append(self.BASE_LENGTH + offset) + for val in values: + if is_float: + payload += struct.pack(" int: - return self.frame[5] - - -class WriteS8HarpMessage(WriteHarpMessage): - def __init__(self, address: int, value: int): - super().__init__( - PayloadType.S8, value.to_bytes(1, byteorder="little", signed=True), address - ) - - @property - def payload(self) -> int: - return int.from_bytes([self.frame[5]], byteorder="little", signed=True) - - -class WriteU16HarpMessage(WriteHarpMessage): - def __init__(self, address: int, value: int): - super().__init__( - PayloadType.U16, value.to_bytes(2, byteorder="little", signed=False), address, offset=1 - ) - - @property - def payload(self) -> int: - return int.from_bytes(self._frame[5:7], byteorder="little", signed=False) - - -class WriteS16HarpMessage(WriteHarpMessage): - def __init__(self, address: int, value: int): - super().__init__( - PayloadType.S16, - value.to_bytes(2, byteorder="little", signed=True), - address, - offset=1, - ) - @property - def payload(self) -> int: - return int.from_bytes(self._frame[5:7], byteorder="little", signed=True) - - -class WriteFloatHarpMessage(WriteHarpMessage): - def __init__(self, address: int, value: float): - super().__init__( - PayloadType.Float, - struct.pack(' float: - return struct.unpack(' int: - return int.from_bytes(self._frame[5:9], byteorder="little", signed=False) - - -class WriteS32HarpMessage(WriteHarpMessage): - def __init__(self, address: int, value: int | List[int]): - if isinstance(value, list): - payload = bytearray() - for val in value: - payload += val.to_bytes(4, byteorder="little", signed=True) - offset = 15 - else: - payload = value.to_bytes(4, byteorder="little", signed=True) - offset = 3 - super().__init__( - PayloadType.S32, payload, address, offset=offset - ) - - @property - def payload(self) -> int | List[int]: - return int.from_bytes(self._frame[5:9], byteorder="little", signed=True) + def payload(self) -> Union[int, list[int]]: + match self.payload_type: + case PayloadType.U8: + return self.frame[5] + case PayloadType.S8: + return int.from_bytes([self.frame[5]], byteorder="little", signed=True) + case PayloadType.U16: + return int.from_bytes( + self._frame[5:7], byteorder="little", signed=False + ) + case PayloadType.S16: + return int.from_bytes(self._frame[5:7], byteorder="little", signed=True) + case PayloadType.Float: + return struct.unpack(" None: # open serial connection and load info - device = Device("/dev/tty.usbserial-A106C8O9") + device = Device("/dev/ttyUSB0", "dump.bin") assert device._ser.is_open device.info() device.disconnect() @@ -17,7 +17,7 @@ def test_create_device() -> None: def test_read_U8() -> None: # open serial connection and load info - device = Device("/dev/tty.usbserial-A106C8O9", "dump.bin") + device = Device("/dev/ttyUSB0", "dump.bin") # read register 38 register: int = 38 @@ -34,7 +34,7 @@ def test_read_U8() -> None: def test_U8() -> None: # open serial connection and load info - device = Device("/dev/tty.usbserial-A106C8O9", "dump.txt") + device = Device("/dev/ttyUSB0", "dump.bin") assert device._dump_file_path.exists() register: int = 38 @@ -44,8 +44,7 @@ def test_U8() -> None: # assert reply[11] == 0 # what is the default register value?! # write 65 on register 38 - write_message = HarpMessage.WriteU8(register, write_value) - reply : ReplyHarpMessage = device.send(write_message.frame) + reply: ReplyHarpMessage = device.write_u8(register, write_value) assert reply is not None # read register 38 diff --git a/tests/test_messages.py b/tests/test_messages.py index 13fc637..93223b4 100644 --- a/tests/test_messages.py +++ b/tests/test_messages.py @@ -1,5 +1,5 @@ from pyharp.base import CommonRegisters, PayloadType -from pyharp.messages import HarpMessage, MessageType, ReadHarpMessage +from pyharp.messages import MessageType, ReadHarpMessage, WriteHarpMessage DEFAULT_ADDRESS = 42 @@ -38,9 +38,9 @@ def test_create_read_S16() -> None: def test_create_write_U8() -> None: value: int = 23 - message = HarpMessage.WriteU8(DEFAULT_ADDRESS, value) + message = WriteHarpMessage(PayloadType.U8, DEFAULT_ADDRESS, value) - assert message.message_type == MessageType.Write + assert message.message_type == MessageType.WRITE assert message.payload == value assert message.checksum == 72 # 2 + 5 + 42 + 255 + 1 + 23 - 256 print(message.frame) @@ -48,9 +48,9 @@ def test_create_write_U8() -> None: def test_create_write_S8() -> None: value: int = -3 # corresponds to signed int 253 (0xFD) - message = HarpMessage.WriteS8(DEFAULT_ADDRESS, value) + message = WriteHarpMessage(PayloadType.S8, DEFAULT_ADDRESS, value) - assert message.message_type == MessageType.Write + assert message.message_type == MessageType.WRITE assert message.payload == value assert message.checksum == 174 # (2 + 5 + 42 + 255 + 129 + 253) & 255 print(message.frame) @@ -58,9 +58,9 @@ def test_create_write_S8() -> None: def test_create_write_U16() -> None: value: int = 1024 # 4 0 (2 x bytes) - message = HarpMessage.WriteU16(DEFAULT_ADDRESS, value) + message = WriteHarpMessage(PayloadType.U16, DEFAULT_ADDRESS, value) - assert message.message_type == MessageType.Write + assert message.message_type == MessageType.WRITE assert message.length == 6 assert message.payload == value assert message.checksum == 55 # (2 + 6 + 42 + 255 + 2 + 4 + 0) & 255 @@ -69,9 +69,9 @@ def test_create_write_U16() -> None: def test_create_write_S16() -> None: value: int = -4837 # 27 237 (2 x bytes), corresponds to signed int 7149 - message = HarpMessage.WriteS16(DEFAULT_ADDRESS, value) + message = WriteHarpMessage(PayloadType.S16, DEFAULT_ADDRESS, value) - assert message.message_type == MessageType.Write + assert message.message_type == MessageType.WRITE assert message.length == 6 assert message.payload == value assert message.checksum == 187 # (2 + 6 + 42 + 255 + 130 + 27 + 237) & 255 From 472e1a5362b3ce1298957311d99854804d8d93b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 2 Apr 2025 09:30:58 +0100 Subject: [PATCH 053/267] Add dump parameter to Device's write methods --- pyharp/device.py | 65 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 19 deletions(-) diff --git a/pyharp/device.py b/pyharp/device.py index f4f160a..b022114 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -322,83 +322,110 @@ def read_float(self, address: int, dump: bool = True) -> ReplyHarpMessage: ReadHarpMessage(payload_type=PayloadType.Float, address=address).frame, dump ) - def write_u8(self, address: int, value: int | List[int]) -> ReplyHarpMessage: + def write_u8( + self, address: int, value: int | List[int], dump: bool = True + ) -> ReplyHarpMessage: return self.send( WriteHarpMessage( payload_type=PayloadType.U8, address=address, value=value, - ).frame + ).frame, + dump=dump, ) - def write_s8(self, address: int, value: int | List[int]) -> ReplyHarpMessage: + def write_s8( + self, address: int, value: int | List[int], dump: bool = True + ) -> ReplyHarpMessage: return self.send( WriteHarpMessage( payload_type=PayloadType.S8, address=address, value=value, - ).frame + ).frame, + dump=dump, ) - def write_u16(self, address: int, value: int | List[int]) -> ReplyHarpMessage: + def write_u16( + self, address: int, value: int | List[int], dump: bool = True + ) -> ReplyHarpMessage: return self.send( WriteHarpMessage( payload_type=PayloadType.U16, address=address, value=value, - ).frame + ).frame, + dump=dump, ) - def write_s16(self, address: int, value: int | List[int]) -> ReplyHarpMessage: + def write_s16( + self, address: int, value: int | List[int], dump: bool = True + ) -> ReplyHarpMessage: return self.send( WriteHarpMessage( payload_type=PayloadType.S16, address=address, value=value, - ).frame + ).frame, + dump=dump, ) - def write_u32(self, address: int, value: int | List[int]) -> ReplyHarpMessage: + def write_u32( + self, address: int, value: int | List[int], dump: bool = True + ) -> ReplyHarpMessage: return self.send( WriteHarpMessage( payload_type=PayloadType.U32, address=address, value=value, - ).frame + ).frame, + dump=dump, ) - def write_s32(self, address: int, value: int | List[int]) -> ReplyHarpMessage: + def write_s32( + self, address: int, value: int | List[int], dump: bool = True + ) -> ReplyHarpMessage: return self.send( WriteHarpMessage( payload_type=PayloadType.S32, address=address, value=value, - ).frame + ).frame, + dump=dump, ) - def write_u64(self, address: int, value: int | List[int]) -> ReplyHarpMessage: + def write_u64( + self, address: int, value: int | List[int], dump: bool = True + ) -> ReplyHarpMessage: return self.send( WriteHarpMessage( payload_type=PayloadType.U64, address=address, value=value, - ).frame + ).frame, + dump=dump, ) - def write_s64(self, address: int, value: int | List[int]) -> ReplyHarpMessage: + def write_s64( + self, address: int, value: int | List[int], dump: bool = True + ) -> ReplyHarpMessage: return self.send( WriteHarpMessage( payload_type=PayloadType.S64, address=address, value=value, - ).frame + ).frame, + dump=dump, ) - def write_float(self, address: int, value: float | List[float]) -> ReplyHarpMessage: + def write_float( + self, address: int, value: float | List[float], dump: bool = True + ) -> ReplyHarpMessage: return self.send( WriteHarpMessage( payload_type=PayloadType.Float, address=address, value=value, - ).frame - ) \ No newline at end of file + ).frame, + dump=dump, + ) From 904040dd710749e5a1be47da258fe2e4be444e77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 2 Apr 2025 10:49:20 +0100 Subject: [PATCH 054/267] Change default name and changed visibility of control registers methods --- examples/check_device_id.py | 4 +- examples/get_info.py | 2 +- pyharp/device.py | 160 ++++++++++++++++++------------------ 3 files changed, 81 insertions(+), 85 deletions(-) diff --git a/examples/check_device_id.py b/examples/check_device_id.py index 7c6bd57..7a64c52 100755 --- a/examples/check_device_id.py +++ b/examples/check_device_id.py @@ -18,14 +18,14 @@ # Get some of the device's parameters device_id = device.WHO_AM_I # Get device's ID -device_id_description = device.WHO_AM_I_DEVICE # Get device's user name +device_id_description = device.DEFAULT_DEVICE_NAME # Get device's user name device_user_name = device.DEVICE_NAME # Get device's user name # Check if we are dealing with the correct device if device_id in device_names: print("Correct device was found!") print(f"Device's ID: {device_id}") - print(f"Device's name: {device_id_description}") + print(f"Device's default name: {device_id_description}") print(f"Device's user name: {device_user_name}") else: print("Device not correct or is not a Harp device!") diff --git a/examples/get_info.py b/examples/get_info.py index 8a33174..113e548 100755 --- a/examples/get_info.py +++ b/examples/get_info.py @@ -20,7 +20,7 @@ # Get some of the device's parameters device_id = device.WHO_AM_I # Get device's ID -device_id_description = device.WHO_AM_I_DEVICE # Get device's user name +device_id_description = device.DEFAULT_DEVICE_NAME # Get device's user name device_user_name = device.DEVICE_NAME # Get device's user name # Get versions diff --git a/pyharp/device.py b/pyharp/device.py index b022114..1250295 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -30,7 +30,7 @@ class Device: _dump_file_path: Path WHO_AM_I: int - WHO_AM_I_DEVICE: str + DEFAULT_DEVICE_NAME: str HW_VERSION_H: int HW_VERSION_L: int ASSEMBLY_VERSION: int @@ -59,20 +59,20 @@ def __init__( self.load() def load(self) -> None: - self.WHO_AM_I = self.read_who_am_i() - self.WHO_AM_I_DEVICE = self.read_who_am_i_device() - self.HW_VERSION_H = self.read_hw_version_h() - self.HW_VERSION_L = self.read_hw_version_l() - self.ASSEMBLY_VERSION = self.read_assembly_version() - self.HARP_VERSION_H = self.read_harp_h_version() - self.HARP_VERSION_L = self.read_harp_l_version() - self.FIRMWARE_VERSION_H = self.read_fw_h_version() - self.FIRMWARE_VERSION_L = self.read_fw_l_version() - self.DEVICE_NAME = self.read_device_name() + self.WHO_AM_I = self._read_who_am_i() + self.DEFAULT_DEVICE_NAME = self._read_default_device_name() + self.HW_VERSION_H = self._read_hw_version_h() + self.HW_VERSION_L = self._read_hw_version_l() + self.ASSEMBLY_VERSION = self._read_assembly_version() + self.HARP_VERSION_H = self._read_harp_h_version() + self.HARP_VERSION_L = self._read_harp_l_version() + self.FIRMWARE_VERSION_H = self._read_fw_h_version() + self.FIRMWARE_VERSION_L = self._read_fw_l_version() + self.DEVICE_NAME = self._read_device_name() def info(self) -> None: print("Device info:") - print(f"* Who am I: ({self.WHO_AM_I}) {self.WHO_AM_I_DEVICE}") + print(f"* Who am I: ({self.WHO_AM_I}) {self.DEFAULT_DEVICE_NAME}") print(f"* HW version: {self.HW_VERSION_H}.{self.HW_VERSION_L}") print(f"* Assembly version: {self.ASSEMBLY_VERSION}") print(f"* HARP version: {self.HARP_VERSION_H}.{self.HARP_VERSION_L}") @@ -97,76 +97,6 @@ def connect(self) -> None: def disconnect(self) -> None: self._ser.close() - def read_who_am_i(self) -> int: - address = CommonRegisters.WHO_AM_I - - reply: ReplyHarpMessage = self.read_u16(address, dump=False) - - return reply.payload_as_int() - - def read_who_am_i_device(self) -> str: - address = CommonRegisters.WHO_AM_I - - reply: ReplyHarpMessage = self.read_u16(address, dump=False) - - return device_names.get(reply.payload_as_int()) - - def read_hw_version_h(self) -> int: - address = CommonRegisters.HW_VERSION_H - - reply: ReplyHarpMessage = self.read_u8(address, dump=False) - - return reply.payload_as_int() - - def read_hw_version_l(self) -> int: - address = CommonRegisters.HW_VERSION_L - - reply: ReplyHarpMessage = self.read_u8(address, dump=False) - - return reply.payload_as_int() - - def read_assembly_version(self) -> int: - address = CommonRegisters.ASSEMBLY_VERSION - - reply: ReplyHarpMessage = self.read_u8(address, dump=False) - - return reply.payload_as_int() - - def read_harp_h_version(self) -> int: - address = CommonRegisters.HARP_VERSION_H - - reply: ReplyHarpMessage = self.read_u8(address, dump=False) - - return reply.payload_as_int() - - def read_harp_l_version(self) -> int: - address = CommonRegisters.HARP_VERSION_L - - reply: ReplyHarpMessage = self.read_u8(address, dump=False) - - return reply.payload_as_int() - - def read_fw_h_version(self) -> int: - address = CommonRegisters.FIRMWARE_VERSION_H - - reply: ReplyHarpMessage = self.read_u8(address, dump=False) - - return reply.payload_as_int() - - def read_fw_l_version(self) -> int: - address = CommonRegisters.FIRMWARE_VERSION_L - - reply: ReplyHarpMessage = self.read_u8(address, dump=False) - - return reply.payload_as_int() - - def read_device_name(self) -> str: - address = CommonRegisters.DEVICE_NAME - - reply: ReplyHarpMessage = self.read_u8(address, dump=False) - - return reply.payload_as_string() - def read_device_mode(self) -> DeviceMode: address = CommonRegisters.OPERATION_CTRL reply = self.read_u8(address) @@ -429,3 +359,69 @@ def write_float( ).frame, dump=dump, ) + + def _read_who_am_i(self) -> int: + address = CommonRegisters.WHO_AM_I + + reply: ReplyHarpMessage = self.read_u16(address, dump=False) + + return reply.payload_as_int() + + def _read_default_device_name(self) -> str: + return device_names.get(self.WHO_AM_I, "Unknown device") + + def _read_hw_version_h(self) -> int: + address = CommonRegisters.HW_VERSION_H + + reply: ReplyHarpMessage = self.read_u8(address, dump=False) + + return reply.payload_as_int() + + def _read_hw_version_l(self) -> int: + address = CommonRegisters.HW_VERSION_L + + reply: ReplyHarpMessage = self.read_u8(address, dump=False) + + return reply.payload_as_int() + + def _read_assembly_version(self) -> int: + address = CommonRegisters.ASSEMBLY_VERSION + + reply: ReplyHarpMessage = self.read_u8(address, dump=False) + + return reply.payload_as_int() + + def _read_harp_h_version(self) -> int: + address = CommonRegisters.HARP_VERSION_H + + reply: ReplyHarpMessage = self.read_u8(address, dump=False) + + return reply.payload_as_int() + + def _read_harp_l_version(self) -> int: + address = CommonRegisters.HARP_VERSION_L + + reply: ReplyHarpMessage = self.read_u8(address, dump=False) + + return reply.payload_as_int() + + def _read_fw_h_version(self) -> int: + address = CommonRegisters.FIRMWARE_VERSION_H + + reply: ReplyHarpMessage = self.read_u8(address, dump=False) + + return reply.payload_as_int() + + def _read_fw_l_version(self) -> int: + address = CommonRegisters.FIRMWARE_VERSION_L + + reply: ReplyHarpMessage = self.read_u8(address, dump=False) + + return reply.payload_as_int() + + def _read_device_name(self) -> str: + address = CommonRegisters.DEVICE_NAME + + reply: ReplyHarpMessage = self.read_u8(address, dump=False) + + return reply.payload_as_string() From 4af5567a62270a70aca420963383477798440850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 2 Apr 2025 10:53:02 +0100 Subject: [PATCH 055/267] Refactor timeout assignment and correct payload string formatting --- pyharp/device.py | 2 +- pyharp/messages.py | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/pyharp/device.py b/pyharp/device.py index 1250295..905e9c1 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -87,7 +87,7 @@ def connect(self) -> None: self._ser = HarpSerial( self._serial_port, # "/dev/tty.usbserial-A106C8O9" baudrate=1000000, - timeout=self.__class__.TIMEOUT_S, + timeout=self.TIMEOUT_S, parity=serial.PARITY_NONE, stopbits=1, bytesize=8, diff --git a/pyharp/messages.py b/pyharp/messages.py index 13b7d01..2c8e780 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -120,8 +120,7 @@ def __str__(self): bytes_per_word = self.payload_type.value & 0x07 format_str = f"0{bytes_per_word}b" - for item in self.payload: - payload_str += f"{item:{format_str}} " + payload_str = "".join(f"{item:{format_str}} " for item in self.payload) return ( f"Type: {self.message_type.name}\r\n" @@ -131,7 +130,7 @@ def __str__(self): + f"Timestamp: {self.timestamp}\r\n" + f"Payload Type: {self.payload_type.name}\r\n" + f"Payload Length: {len(self.payload)}\r\n" - + f"Payload: {self.payload}\r\n" + + f"Payload: {payload_str}\r\n" + f"Checksum: {self.checksum}" ) @@ -245,7 +244,7 @@ def __init__( def payload(self) -> Union[int, list[int]]: match self.payload_type: case PayloadType.U8: - return self.frame[5] + return self._frame[5] case PayloadType.S8: return int.from_bytes([self.frame[5]], byteorder="little", signed=True) case PayloadType.U16: @@ -262,7 +261,6 @@ def payload(self) -> Union[int, list[int]]: ) case PayloadType.S32: return int.from_bytes(self._frame[5:9], byteorder="little", signed=True) - # TODO: missing validation for U64 and S64 case PayloadType.U64: return int.from_bytes( self._frame[5:13], byteorder="little", signed=False From a2b8cc092b2bf77ffc41474ab0c780d07a95a9e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 2 Apr 2025 13:53:22 +0100 Subject: [PATCH 056/267] Update current_device_names --- pyharp/device_names.py | 68 ++++++++++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/pyharp/device_names.py b/pyharp/device_names.py index d7b7557..c166ff0 100644 --- a/pyharp/device_names.py +++ b/pyharp/device_names.py @@ -1,26 +1,50 @@ from collections import defaultdict +# This file contains the device names for the current version of the pyHarp library. +# These names were extracted from https://github.com/harp-tech/protocol/blob/main/whoami.yml +# commit used: https://github.com/harp-tech/protocol/commit/3e2a228 -current_device_names = \ - {1024: 'Poke', - 1040: 'MultiPwm', - 1056: 'Wear', - 1072: 'VoltsDrive', - 1088: 'LedController', - 1104: 'Synchronizer', - 1121: 'SimpleAnalogGenerator', - 1136: 'Archimedes', - 1152: 'ClockSynchronizer', - 1168: 'Camera', - 1184: 'PyControl', - 1200: 'FlyPad', - 1216: 'Behavior', - 1232: 'LoadCells', - 1248: 'AudioSwitch', - 1264: 'Rgb', - 1200: 'FlyPad', - 2064: 'FP3002', - 2080: 'IblBehavior'} -device_names = defaultdict(lambda: 'NotSpecified') -device_names.update(current_device_names) +current_device_names = { + 256: "USBHub", + 1024: "Poke", + 1040: "MultiPwmGenerator", + 1056: "Wear", + 1058: "WearBaseStationGen2", + 1072: "Driver12Volts", + 1088: "LedController", + 1104: "Synchronizer", + 1106: "InputExpander", + 1108: "OutputExpander", + 1121: "SimpleAnalogGenerator", + 1130: "StepperDriver", + 1136: "Archimedes", + 1140: "Olfactometer", + 1152: "ClockSynchronizer", + 1154: "TimestampGeneratorGen1", + 1158: "TimestampGeneratorGen3", + 1168: "CameraController", + 1170: "CameraControllerGen2", + 1184: "PyControlAdapter", + 1200: "FlyPad", + 1216: "Behavior", + 1224: "VestibularH1", + 1225: "VestibularH2", + 1232: "LoadCells", + 1236: "AnalogInput", + 1248: "RgbArray", + 1280: "SoundCard", + 1296: "SyringePump", + 1400: "LicketySplit", + 1401: "SniffDetector", + 1402: "Treadmill", + 1403: "cuTTLefish", + 1404: "WhiteRabbit", + 1405: "EnvironmentSensor", + 2064: "NeurophotometricsFP3002", + 2080: "Ibl_behavior_control", + 2094: "RfidReader", + 2110: "Pluma", +} +device_names = defaultdict(lambda: "NotSpecified") +device_names.update(current_device_names) From 45ee96543469788837ab72c939fea952e73004bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 2 Apr 2025 13:55:13 +0100 Subject: [PATCH 057/267] Add support for device's serial number --- examples/get_info.py | 1 + pyharp/base.py | 1 + pyharp/device.py | 14 ++++++++++++++ 3 files changed, 16 insertions(+) diff --git a/examples/get_info.py b/examples/get_info.py index 113e548..05ed9f9 100755 --- a/examples/get_info.py +++ b/examples/get_info.py @@ -31,6 +31,7 @@ device_harp_h = device.HARP_VERSION_H # Get device's harp core version device_harp_l = device.HARP_VERSION_L # Get device's harp core version device_assembly = device.ASSEMBLY_VERSION # Get device's assembly version +device_serial_number = device.SERIAL_NUMBER # Get device's serial number reg_dump = device.dump_registers() for reg_reply in reg_dump: diff --git a/pyharp/base.py b/pyharp/base.py index 271a903..af4219f 100644 --- a/pyharp/base.py +++ b/pyharp/base.py @@ -50,3 +50,4 @@ class CommonRegisters: OPERATION_CTRL = 0x0A RESET_DEV = 0x0B DEVICE_NAME = 0x0C + SERIAL_NUMBER = 0x0D diff --git a/pyharp/device.py b/pyharp/device.py index 905e9c1..1718394 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -39,6 +39,7 @@ class Device: FIRMWARE_VERSION_H: int FIRMWARE_VERSION_L: int DEVICE_NAME: str + SERIAL_NUMBER: int TIMEOUT_S = 1.0 @@ -69,6 +70,7 @@ def load(self) -> None: self.FIRMWARE_VERSION_H = self._read_fw_h_version() self.FIRMWARE_VERSION_L = self._read_fw_l_version() self.DEVICE_NAME = self._read_device_name() + self.SERIAL_NUMBER = self._read_serial_number() def info(self) -> None: print("Device info:") @@ -78,6 +80,7 @@ def info(self) -> None: print(f"* HARP version: {self.HARP_VERSION_H}.{self.HARP_VERSION_L}") print(f"* Firmware version: {self.FIRMWARE_VERSION_H}.{self.FIRMWARE_VERSION_L}") print(f"* Device user name: {self.DEVICE_NAME}") + print(f"* Serial number: {self.SERIAL_NUMBER}") print(f"* Mode: {self.read_device_mode().name}") def read(self): @@ -425,3 +428,14 @@ def _read_device_name(self) -> str: reply: ReplyHarpMessage = self.read_u8(address, dump=False) return reply.payload_as_string() + + def _read_serial_number(self) -> int: + address = CommonRegisters.SERIAL_NUMBER + + reply: ReplyHarpMessage = self.read_u8(address, dump=False) + + if reply.has_error(): + return 0 + + return reply.payload_as_int() + From 20c2c59449b602a8bde6d88b0638a640a55a975f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 2 Apr 2025 13:58:52 +0100 Subject: [PATCH 058/267] Remove unused method --- pyharp/device.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyharp/device.py b/pyharp/device.py index 1718394..57b7191 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -83,9 +83,6 @@ def info(self) -> None: print(f"* Serial number: {self.SERIAL_NUMBER}") print(f"* Mode: {self.read_device_mode().name}") - def read(self): - pass - def connect(self) -> None: self._ser = HarpSerial( self._serial_port, # "/dev/tty.usbserial-A106C8O9" From 1aecf2a3fe25a2796e58a500bba35861c2c35563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 2 Apr 2025 16:12:12 +0100 Subject: [PATCH 059/267] Update README --- README.md | 96 ++++++++++++++++++++----------------------------------- 1 file changed, 35 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 9191782..335330f 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,56 @@ # pyharp -Harp implementation of the Harp protocol. +Python implementation of the Harp protocol for hardware control and data acquisition. -> [!CAUTION] -> The README is currently outdated! - -## Install with Pip -From this directory, install in editable mode with -```` -pip install -e . -```` - -Note that for the above to work, a fairly recent version of pip (>= 21.3) is required. - -## Install with Poetry - -Each Python user has is own very dear IDE for editing. Here, we are leaving instructions on how to edit this code using pyCharm, Anaconda and Poetry. - -The instructions are for beginner. Most of the users can just skip them. - -This was tested on a Windows machine, but should be similar to other systems. +## Installation +```bash +uv add pyharp +# or +pip install pyharp +``` -### 1. Install PyCHarm -**PyCharm** can be download from [here](https://www.jetbrains.com/pycharm/download/). The Community version is enough. -Download and install it. +## Quick Start -### 2. Install Anaconda +```python +from pyharp.device import Device -**Anaconda** can be found [here](https://www.anaconda.com/products/individual). -Download the version according to your computer and install it. -- Unselect **Add Anaconda to the system PATH environment variable** -- Select ** Register Anaconda as the system Pyhton** +# Connect to a device +device = Device("/dev/ttyUSB0") -It's suggested to reboot your computer at this point +# Get device information +device.info() -### 3. Install Poetry +# define register_address +register_address = 32 -Open the **Command Prompt** and execute the next command: -``` -curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python -``` +# Read from register +value = device.read_u8(register_address) -### 4. Install pyharp +# Write to register +device.write_u8(register_address, value) -Open **Anaconda**, navigate to the repository folder and execute the next commands: -``` -poetry install -poetry env info +# Disconnect when done +device.disconnect() ``` -The second comand will reply with a **Path:**. -Select and copy this path. +or using the `with` statement: -### 5. Using PyCharm to edit the code +```python +from pyharp.device import Device -1. Open **PyCharm** :) -2. Go to File -> Open, select the repository folder, and click **OK** -3. Go to File -> Settings -> Project:pyharp -> Project Interpreter -3.1 Click in the gear in front of the Project Interpreter: and select **Add...** -3.2 On Virtualenv Environment, chose Existing environment -3.3 Select **python.exe** on the folder Scripts under the path copied from the _poetry env info_ command -3.4 Click **OK** and **OK** +with Device("/dev/ttyUSB0") as device: + # Get device information + device.info() -You are ready to go! + # define register_address + register_address = 32 -### 6. Test the code + # Read from register + value = device.read_u8(register_address) -Under **PyCharm**, Open one of the examples from the folder _examples_ (the _get_info.py_ is generic, so it's a good option) and update the COMx to your COM number. -Right-click on top of the file and chose option _Run 'get_info.py_. You should read something like this in the console: -``` -Device info: -* Who am I: (2080) IblBehavior -* HW version: 1.0 -* Assembly version: 0 -* HARP version: 1.6 -* Firmware version: 1.0 -* Device user name: IBL_rig_0 + # Write to register + device.write_u8(register_address, value) ``` ## for Linux @@ -85,7 +59,7 @@ Device info: Install by either copying `10-harp.rules` over to your `/etc/udev/rules.d` folder or by symlinking it with: ```` -sudo ln -s /absolute/path/to/vibratome-controller/10-harp.rules /etc/udev/rules.d/10-harp.rules +sudo ln -s /absolute/path/to/10-harp.rules /etc/udev/rules.d/10-harp.rules ```` Then reload udev rules with From b9819aa54a61de2dc50d01c022437bbc8e8dac48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 2 Apr 2025 16:13:45 +0100 Subject: [PATCH 060/267] Update documentation dependencies --- pyproject.toml | 3 ++ uv.lock | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 14f8aa5..b124dea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,9 @@ dependencies = [ [dependency-groups] dev = [ "mkdocs>=1.6.1", + "mkdocs-codeinclude-plugin>=0.2.1", + "mkdocs-git-authors-plugin>=0.9.4", + "mkdocs-git-committers-plugin-2>=2.5.0", "mkdocs-material>=9.6.9", "mkdocstrings-python>=1.16.6", "pytest>=8.3.5", diff --git a/uv.lock b/uv.lock index 984b6c6..046433d 100644 --- a/uv.lock +++ b/uv.lock @@ -114,6 +114,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034 }, ] +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794 }, +] + +[[package]] +name = "gitpython" +version = "3.1.44" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/89/37df0b71473153574a5cdef8f242de422a0f5d26d7a9e231e6f169b4ad14/gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269", size = 214196 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599 }, +] + [[package]] name = "griffe" version = "1.6.1" @@ -260,6 +284,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f8/29/1125f7b11db63e8e32bcfa0752a4eea30abff3ebd0796f808e14571ddaa2/mkdocs_autorefs-1.4.1-py3-none-any.whl", hash = "sha256:9793c5ac06a6ebbe52ec0f8439256e66187badf4b5334b5fde0b128ec134df4f", size = 5782047 }, ] +[[package]] +name = "mkdocs-codeinclude-plugin" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mkdocs" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/b5/f72df157abc7f85e33ffa417464e9dd535ef5fda7654eda41190047a53b6/mkdocs-codeinclude-plugin-0.2.1.tar.gz", hash = "sha256:305387f67a885f0e36ec1cf977324fe1fe50d31301147194b63631d0864601b1", size = 8140 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/7b/60573ebf2a22b144eeaf3b29db9a6d4d342d68273f716ea2723d1ad723ba/mkdocs_codeinclude_plugin-0.2.1-py3-none-any.whl", hash = "sha256:172a917c9b257fa62850b669336151f85d3cd40312b2b52520cbcceab557ea6c", size = 8093 }, +] + [[package]] name = "mkdocs-get-deps" version = "0.2.0" @@ -274,6 +311,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521 }, ] +[[package]] +name = "mkdocs-git-authors-plugin" +version = "0.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/9a/063c4a3688e4669eb2054e4bf6e9cc582f6c1d85674e3f5b836ceff97c3b/mkdocs_git_authors_plugin-0.9.4.tar.gz", hash = "sha256:f5cfaf93d08981ce25591bbaf642051ed168c3886bb96ecd2dca53f0ef1973b8", size = 21914 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/ac/2b5bae4047276fda2bdd14a6d4af59288fb4d5de54151ae4e6ba17611ceb/mkdocs_git_authors_plugin-0.9.4-py3-none-any.whl", hash = "sha256:84b9b56c703841189c64d8ff6947034fe0a9c14a0a8f1f6255edfcfe3a56825f", size = 20752 }, +] + +[[package]] +name = "mkdocs-git-committers-plugin-2" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitpython" }, + { name = "mkdocs" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/8a/4ca4fb7d17f66fa709b49744c597204ad03fb3b011c76919564843426f11/mkdocs_git_committers_plugin_2-2.5.0.tar.gz", hash = "sha256:a01f17369e79ca28651681cddf212770e646e6191954bad884ca3067316aae60", size = 15183 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/f5/768590251839a148c188d64779b809bde0e78a306295c18fc29d7fc71ce1/mkdocs_git_committers_plugin_2-2.5.0-py3-none-any.whl", hash = "sha256:1778becf98ccdc5fac809ac7b62cf01d3c67d6e8432723dffbb823307d1193c4", size = 11788 }, +] + [[package]] name = "mkdocs-material" version = "9.6.9" @@ -401,6 +464,9 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "mkdocs" }, + { name = "mkdocs-codeinclude-plugin" }, + { name = "mkdocs-git-authors-plugin" }, + { name = "mkdocs-git-committers-plugin-2" }, { name = "mkdocs-material" }, { name = "mkdocstrings-python" }, { name = "pytest" }, @@ -413,6 +479,9 @@ requires-dist = [{ name = "pyserial", specifier = ">=3.5" }] [package.metadata.requires-dev] dev = [ { name = "mkdocs", specifier = ">=1.6.1" }, + { name = "mkdocs-codeinclude-plugin", specifier = ">=0.2.1" }, + { name = "mkdocs-git-authors-plugin", specifier = ">=0.9.4" }, + { name = "mkdocs-git-committers-plugin-2", specifier = ">=2.5.0" }, { name = "mkdocs-material", specifier = ">=9.6.9" }, { name = "mkdocstrings-python", specifier = ">=1.16.6" }, { name = "pytest", specifier = ">=8.3.5" }, @@ -564,6 +633,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, ] +[[package]] +name = "smmap" +version = "5.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303 }, +] + [[package]] name = "urllib3" version = "2.3.0" From 2ca3e4f3bb81d1593f1996b6e482b36b7dc9e59f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 2 Apr 2025 16:15:41 +0100 Subject: [PATCH 061/267] Update mkdocs configuration --- mkdocs.yml | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/mkdocs.yml b/mkdocs.yml index e4b6a6e..e474f18 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,16 +1,61 @@ site_name: pyharp +repo_url: "https://github.com/fchampalimaud/pyharp" +repo_name: "pyharp" plugins: + - search + - autorefs + - codeinclude - mkdocstrings: handlers: python: options: docstring_style: numpy + show_root_heading: true + show_submodules: true + show_source: false + - git-committers: + repository: fchampalimaud/pyharp + branch: main + - git-authors + markdown_extensions: + - abbr + - attr_list - admonition - pymdownx.details - pymdownx.superfences + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - toc: + permalink: "#" theme: name: material + icon: + repo: fontawesome/brands/github + features: + - content.tooltips + - toc.follow + palette: + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Switch to light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: blue + accent: blue + toggle: + icon: material/weather-sunny + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: red + accent: red + toggle: + icon: material/weather-night + name: Switch to system preference + From fb78b31e8146eb7b231811f9486b8cbf3ca49f63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 2 Apr 2025 16:16:55 +0100 Subject: [PATCH 062/267] Move examples to documentation folder --- .../examples/code}/check_device_id.py | 2 +- {examples => docs/examples/code}/get_info.py | 0 .../examples/code}/wait_for_events.py | 0 .../code}/write_and_read_from_registers.py | 0 docs/examples/examples.md | 42 ++++++++++++++++ examples/behavior_device_driver_test.py | 49 ------------------- 6 files changed, 43 insertions(+), 50 deletions(-) rename {examples => docs/examples/code}/check_device_id.py (95%) rename {examples => docs/examples/code}/get_info.py (100%) rename {examples => docs/examples/code}/wait_for_events.py (100%) rename {examples => docs/examples/code}/write_and_read_from_registers.py (100%) create mode 100644 docs/examples/examples.md delete mode 100755 examples/behavior_device_driver_test.py diff --git a/examples/check_device_id.py b/docs/examples/code/check_device_id.py similarity index 95% rename from examples/check_device_id.py rename to docs/examples/code/check_device_id.py index 7a64c52..04297a6 100755 --- a/examples/check_device_id.py +++ b/docs/examples/code/check_device_id.py @@ -12,7 +12,7 @@ # Open the device # Open serial connection if os.name == "posix": # check for Linux. - device = Device("/dev/harp_device_00") + device = Device("/dev/ttyUSB0") else: # assume Windows. device = Device("COM95") diff --git a/examples/get_info.py b/docs/examples/code/get_info.py similarity index 100% rename from examples/get_info.py rename to docs/examples/code/get_info.py diff --git a/examples/wait_for_events.py b/docs/examples/code/wait_for_events.py similarity index 100% rename from examples/wait_for_events.py rename to docs/examples/code/wait_for_events.py diff --git a/examples/write_and_read_from_registers.py b/docs/examples/code/write_and_read_from_registers.py similarity index 100% rename from examples/write_and_read_from_registers.py rename to docs/examples/code/write_and_read_from_registers.py diff --git a/docs/examples/examples.md b/docs/examples/examples.md new file mode 100644 index 0000000..1de83a0 --- /dev/null +++ b/docs/examples/examples.md @@ -0,0 +1,42 @@ +# Examples + +## Getting Device Info + +This example demonstrates connecting to a Harp device and reading its information: + + +```python +[](./code/get_info.py) +``` + + + +## Check Device Id + +This example demonstrates connecting to a Harp device and checking its ID: + + +```python +[](./code/check_device_id.py) +``` + + +## Wait for Events + +This example demonstrates connecting to a Harp device and waiting for events: + + +```python +[](./code/wait_for_events.py) +``` + + +## Write and Read from registers + +This example demonstrates connecting to a Harp device and writing and reading from registers: + + +```python +[](./code/write_and_read_from_registers.py) +``` + \ No newline at end of file diff --git a/examples/behavior_device_driver_test.py b/examples/behavior_device_driver_test.py deleted file mode 100755 index dd6a3a4..0000000 --- a/examples/behavior_device_driver_test.py +++ /dev/null @@ -1,49 +0,0 @@ -import os - -from pyharp.drivers.behavior import Behavior - -# Open the device and print the info on screen -# Open serial connection and save communication to a file -device = None -if os.name == "posix": # check for Linux. - device = Behavior("/dev/ttyUSB0", "ibl.bin") -else: # assume Windows. - device = Behavior("COM95", "ibl.bin") - -print(f"digital inputs: {device.all_input_states:03b}") -print(f"digital outputs: {device.all_output_states:016b}") -print(f"setting digital outputs") -# device.all_output_states = 0x0000 # Set the whole port directly. -# device.set_outputs(0xFFFF) # Set the values set to logic 1 only. -# device.clear_outputs(0xFFFF)# Clear values set to logic 1 only. -print(f"digital outputs: {device.all_output_states:016b}") -# device.set_io_configuration(0b111) - -# TODO: FIXME. IOs are not working -# device.set_io_configuration(0b111) # This is getting ignored? -# device.set_io_outputs(0b000) -# device.all_io_states = 0b000 -# print(f"digital ios: {device.all_io_states:03b}") - -# device.D0 = 0 -# print(f"D0: {device.D0}") -# device.D0 = 1 -# print(f"D0: {device.D0}") -# -# device.D1 = 0 -# print(f"D1: {device.D1}") -# device.D1 = 1 -# print(f"D1: {device.D1}") -# -# print(f"DI2: {device.DI2}") - - -# import time -# while True: -# print(f"PORT0 IN State: {device.port0_i0}") -# print(f"PORT0 IO State: {device.port0_io0}") -# print(f"PORT0 OUT State: {device.port0_o0}") -# print(f"all port io states: {device.all_port_io_states}") -# print(f"all port output states: {device.all_port_output_states}") -# print() -# time.sleep(0.1) From 4dd42e8d1613df4553534d4cf1c0fa8427ca4d20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 2 Apr 2025 16:17:30 +0100 Subject: [PATCH 063/267] Update documentation --- docs/api/device.md | 1 + docs/api/messages.md | 4 ++++ docs/index.md | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 docs/api/device.md create mode 100644 docs/api/messages.md diff --git a/docs/api/device.md b/docs/api/device.md new file mode 100644 index 0000000..9e949dc --- /dev/null +++ b/docs/api/device.md @@ -0,0 +1 @@ +::: pyharp.device.Device \ No newline at end of file diff --git a/docs/api/messages.md b/docs/api/messages.md new file mode 100644 index 0000000..17f4233 --- /dev/null +++ b/docs/api/messages.md @@ -0,0 +1,4 @@ +::: pyharp.messages.HarpMessage +::: pyharp.messages.ReplyHarpMessage +::: pyharp.messages.ReadHarpMessage +::: pyharp.messages.WriteHarpMessage \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index c0f9b06..f48b45f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,4 @@ -# pyharp +# Introduction !!! Warning Work in Progress! \ No newline at end of file From 8cd50ad717642b302e9037e2ba991de14f0966cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 2 Apr 2025 16:19:12 +0100 Subject: [PATCH 064/267] Add context manager support to Device class --- pyharp/device.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/pyharp/device.py b/pyharp/device.py index 57b7191..0ea24d4 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -436,3 +436,30 @@ def _read_serial_number(self) -> int: return reply.payload_as_int() + def __enter__(self): + """Support for using Device with 'with' statement. + + Returns + ------- + Device + The Device instance + """ + # Connection is already established in __init__ + # but we could add additional setup if needed + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Cleanup resources when exiting the 'with' block. + + Parameters + ---------- + exc_type : Exception type or None + Type of the exception that caused the context to be exited + exc_val : Exception or None + Exception instance that caused the context to be exited + exc_tb : traceback or None + Traceback if an exception occurred + """ + self.disconnect() + # Return False to propagate exceptions that occurred in the with block + return False \ No newline at end of file From 13924db15e1eee430faca1f3179bd068d940a2f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Grilo?= Date: Fri, 4 Apr 2025 17:58:33 +0100 Subject: [PATCH 065/267] Chore: add pytest configs to vscode workspace settings --- .vscode/settings.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index dec7265..fcf64d1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,5 +5,10 @@ "ruff.organizeImports": true, "editor.codeActionsOnSave": { "source.organizeImports": "explicit" - } + }, + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true } \ No newline at end of file From 413f36c24c9f36e389f2792e91463860fcc49a05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Grilo?= Date: Fri, 4 Apr 2025 18:02:48 +0100 Subject: [PATCH 066/267] Docs: separate examples in different pages --- docs/examples/check_device_id.md | 9 ++++ docs/examples/examples.md | 42 ------------------- docs/examples/get_info.md | 9 ++++ docs/examples/index.md | 10 +++++ docs/examples/wait_for_events.md | 9 ++++ .../examples/write_and_read_from_registers.md | 9 ++++ 6 files changed, 46 insertions(+), 42 deletions(-) create mode 100644 docs/examples/check_device_id.md delete mode 100644 docs/examples/examples.md create mode 100644 docs/examples/get_info.md create mode 100644 docs/examples/index.md create mode 100644 docs/examples/wait_for_events.md create mode 100644 docs/examples/write_and_read_from_registers.md diff --git a/docs/examples/check_device_id.md b/docs/examples/check_device_id.md new file mode 100644 index 0000000..c9946ec --- /dev/null +++ b/docs/examples/check_device_id.md @@ -0,0 +1,9 @@ +# Check Device Id + +This example demonstrates connecting to a Harp device and checking its ID: + + +```python +[](./code/check_device_id.py) +``` + \ No newline at end of file diff --git a/docs/examples/examples.md b/docs/examples/examples.md deleted file mode 100644 index 1de83a0..0000000 --- a/docs/examples/examples.md +++ /dev/null @@ -1,42 +0,0 @@ -# Examples - -## Getting Device Info - -This example demonstrates connecting to a Harp device and reading its information: - - -```python -[](./code/get_info.py) -``` - - - -## Check Device Id - -This example demonstrates connecting to a Harp device and checking its ID: - - -```python -[](./code/check_device_id.py) -``` - - -## Wait for Events - -This example demonstrates connecting to a Harp device and waiting for events: - - -```python -[](./code/wait_for_events.py) -``` - - -## Write and Read from registers - -This example demonstrates connecting to a Harp device and writing and reading from registers: - - -```python -[](./code/write_and_read_from_registers.py) -``` - \ No newline at end of file diff --git a/docs/examples/get_info.md b/docs/examples/get_info.md new file mode 100644 index 0000000..96d03e9 --- /dev/null +++ b/docs/examples/get_info.md @@ -0,0 +1,9 @@ +# Getting Device Info + +This example demonstrates connecting to a Harp device and reading its information: + + +```python +[](./code/get_info.py) +``` + diff --git a/docs/examples/index.md b/docs/examples/index.md new file mode 100644 index 0000000..6c4ac5b --- /dev/null +++ b/docs/examples/index.md @@ -0,0 +1,10 @@ +# Examples + +This section contains some examples to help you get started with `pyharp`. + +Here's the complete list of available examples: + +- [Getting Device Info](get_info.md) - connect to a Harp device and read its information. +- [Check Device ID](check_device.md) - connect to a Harp device and check its ID. +- [Wait for Events](wait_for_events.md) - connect to a Harp device and wait for events. +- [Write and Read from Registers](write_and_read_from_registers.md) - connect to a Harp device and read from registers. \ No newline at end of file diff --git a/docs/examples/wait_for_events.md b/docs/examples/wait_for_events.md new file mode 100644 index 0000000..a40c86a --- /dev/null +++ b/docs/examples/wait_for_events.md @@ -0,0 +1,9 @@ +# Wait for Events + +This example demonstrates connecting to a Harp device and waiting for events: + + +```python +[](./code/wait_for_events.py) +``` + \ No newline at end of file diff --git a/docs/examples/write_and_read_from_registers.md b/docs/examples/write_and_read_from_registers.md new file mode 100644 index 0000000..55f9e64 --- /dev/null +++ b/docs/examples/write_and_read_from_registers.md @@ -0,0 +1,9 @@ +# Write and Read from registers + +This example demonstrates connecting to a Harp device and writing and reading from registers: + + +```python +[](./code/write_and_read_from_registers.py) +``` + \ No newline at end of file From f0266ccc9de17f2f4ccaa7b2ab1303fc4fa499f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Grilo?= Date: Fri, 4 Apr 2025 18:05:23 +0100 Subject: [PATCH 067/267] Docs: modify documentation organizational, functional and visual aspects --- docs/assets/favicon.png | Bin 0 -> 14859 bytes docs/assets/logo.svg | 71 +++++++++++++++++++++++++++++++++++++ docs/stylesheets/extra.css | 9 +++++ mkdocs.yml | 65 +++++++++++++++++++++++---------- 4 files changed, 126 insertions(+), 19 deletions(-) create mode 100644 docs/assets/favicon.png create mode 100644 docs/assets/logo.svg create mode 100644 docs/stylesheets/extra.css diff --git a/docs/assets/favicon.png b/docs/assets/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..49c6faef9130e5bdf4a397eb92d94e0e8a3c85d0 GIT binary patch literal 14859 zcmeHuS5%W-_h%FptjLE*FDe}=0xu{Xr5LJGrK9vNAOu1c3raT#p-68cP3aIqY?RPq zf*1)9gwR3@kc1wX2fw-b-_2SxYp&jl@GO$&l)cZczrD}vdnWql&+wc9fk5ZMcXiA_ zpyM+~AC^iFvtxAfk0%u z{C$F59zJ%H2?+AYT~*@&frLO{9c_!Scgqvu@9abJD2H3~5t4hyRh(HS@^qNBl}@r< z%8uY*yL2zcG3FF!rp0kR?Z7ttY)b$0g%RzZ^Y_27JQ2}$|2Wdt$09DCh+yfxtfP=q zTE))P3K~TW?Tl;wCyNDK(elAGWMpY4?0Y;bt`f?Qt8l8+2uW!}0GkATzl&u89`JOftlVv}^!9BKJ1qCeSUd^4teaHX&=uZBjay)At|q*h zRUr-@KCD1Jy|6jbT@CqoE(t>j$BgoGPP6J?%?vux@<{tPx$t z3)^d<4zfq=cSI@cupW=KTKgWTiI*CrU4^II+Yg*{Yd2lhywSkBQ8OyN!nbfDJa2S^ zZeCAl#bDFsG_4<4cQqz=Iyjx!ZrQ9WSSLQbmqW;b+5Bqf1)Gfo3 zy}?4I?J+VuiEqmRhlCvpmC^DDr73n%wHpiflfOzxl?=!@-nx>jw0;7Pvc=s1hi2vK z9!fv)AUUY$mAiEOP#xCu>uZlV{056s-y%SeY959(t)vT8)j3%0>}!!e#}VOC_uD_24O!w(#&~C^heaz4# z!=TnYvZtz06}9AY-DgTu3MYOT{>d$>$97845O>-|O#2k=9GGIzH)-?1A|Rtt*m}jT zX7}qAZH?Q_PgVlwx~4DhocL{5{-RR+a;D_S*o-YKAb&03UH{mAbBu>O&Z~BM!fQ{e zzs5HJP0T56DKA4Rw^yWwwUOqrC9!(kq)EnC@rKnVStThASgEyMKqY%HSp?P#5&_N2je0AmFV$c`Uz>sI*WEVYSj%MS(hiPk2sh)hY}*_|NO z+poc5Iuz~e2=Bq+`qf2i*na4Nn%b9v{eL7lwp=ram{GiyNveRN{fQlcwbN^tvW?EB z8*F7r`Z)2cm**{SPwzkby3Y*D=R{V#PEMzFDuzsBd~GJ5VqId3>ia$*ciNxjB`>W; z=@K$mGrWpCTO7E>MiJ(R-zVR;M@(K)cfYxvsshqDcB&CowQT~N zPM47$19JS?;{mdkyF*F&$In?+A}E&Nvc2tgCUAY1q#Lk54tG45pb5or$qf(I8cd7` z5^(>->!os^(HMn1HI2OzZb*3V^uTQ*xH!JC*vxIEYG+#Qcx|POQ>JuH^a#{SahL&$ zMLjh17ld1WaC%IoysA_EvB~M;8#>tpL)xuurZCv#hu6Vm{3woyA#Bh~qQq$Jf6nfw z|F-nWcMpcvk=UK@UNG&Wh=*8XUJKwPtLwcV{c=F|T1MJM1`6E5-x}@q(rjGST;)|i z879AUK11@d6k(HSG|n#g0SE7Z62&Msu8Ot!(=+5Xh7)ZANr5FQ_V0#RM3U_;gib^BTLD;lCecRL9_yzuzJyD`MT?9up&pIkO!b&F6z2b!Z#u+t$RiN&8P~ z`XF2~EQ8I!ny{sh3(|hR1_oU%BBV&;pAH{r(GPHBtm=>JzRkjAqsYVF5sSDiqE|!J zq~A-`$YwWTHd3-%`QaOPdbAu=U*S`kPl|!$#MeqQbtzg3 z>n_z1esEX81V`MEk%|P}v_oWfFyD`J=6l%1hAvX(X5d**FmSBg=;74w862_A&eqlH z@8$NC!8WwZiWzszBsH!Oe~+C@xx0uG*{E;z;{Zm!w`lEj&#>InWqn=GxbSi0>_nqI zu5>{iw1}w<3$q1C5nka(o;2AJ-p3ocYruQ*R?f?@o|O$^b6fpgTU*Af*81Z= ztd6Xc{Gkr;;6zovpd`N!e|trGxvvAPsuC z^7-v@d~*BJP3@G6GW_X&?}t)}_f11}C#mDUZQE*0gzuY61|Ad007bGJ)(4dITieEK z{GVpmi5aLSTT!f>|8A?p`dZ@xKmD(+pi$TGKY@99x>7&&eR%D^DT_4(C0&50n2@Lm zU@Of0m0mV7H@Z2WUHwtxC|ocnXJcO_FfXuhf)A#+ssOt!lY6Sre-7}GhYk6rT4~oZ zcM!gx46q7%iC}m0=Fr6ccgOttFo}+!!VBXX4-N5rwsrAhy=;D=fM%M}USVO9Q51Rn zQV9eAvF`t)!SM@_!H_mIr*_I?;#K&G#@Q;?%>9DDKdZ>8{?Juk zsyizR!i4nAAMcDWod4$5{{fRh`?A0f0u^2nb<39F9W=LQ2~tcnU+HH}Zu1EpdsKEE zUj~Gh0RYyPJcTo?FN^X>`9)%z@w}YgATicG?j<$)ZZT4spBD)rbc&e>4L8GR6y8(q zvUW=O)COKJfa}cO#vSts`lq8+gyJxF*}J8AlJ&3lV%6*`00VR*cu_y6aU~M0@OPWy zp`8xct&TSuXS7q?>)`^qP$Sx_FMw^V=-r>dsve$#gNnHw%02_ZD$?nGYePhg)-G7H0Y6eHw4XnzI`pHuUkzsNTv~z7CZF^^p zp!tTFbj2%B-}EuV+${|YTpspLXjkX{kDrP*0smbl898=&$;_$r(R&n3NHD)fX2P?c z_iTSmDVle6y)E9C_YVoKb`S%E)lrI_p^xQNab@g-A+XlUfyv&En>xlkGhQ3Ng1g#C zAkd%6e21Koh~l4gWYNLxxJb`cr3Tqq(uP0`wHkG9{#YcCNQf{zds1b8!$S5-*Sol3 zC~LDG&lLWuj*m82-q0@STanRln7%2u&lxZ(1kZ5atwF~~nX=B!@w#@o^cnKL9VySe zp1aysF1~Y4_DNyx1x4~jFy@yyF`o-WNbo8{Oa{2KW-Yat?Uu9!RG00)=}ARN;(lHq zwFv)ImwVw!3zIQ4rTMV8_>iL;_I248d?H*ui*{CF@7`fYieS>F3x$P-8S9dg1rLkn zN$orW59{xRcVCin*Ejv0;cGrA<)9%3?crKy2IF(c<>SllnpGszZ&5-=o;rJ0He`4` zNK@C${9p$ZD~c5(+XkZ~TJ}W?l;=}{m;T00HJg(ToATKvzcxAV%6&YpOeu!j<}FsnS-s<;56NcMmk<&>C?#%K>U06CXy2$!3m}59}6U zEH!vo+eyB$0hH}^nJOAm9PD9mpa(V;OrH1DCYfWMGHI2-W@_u}ecD_FrJDWr_6QwA z{Y(5LRcf%x2glXOrtwKJ^P2mZF(Z3utwTqBC2`+deFQ8YV@XR9r5BcVbv64nP{Z>A z)`Re=i}YKhM2oOC>RhB_Pu!#7=NG_~%*I*)%t?^T=MtEP%}h=PW=X99rMC1Eo3QS% z9@K-O2K9_3_B1aCxCdnXw5yxv8MdFd!Td0xPjPHiK(A+`I%6E?w@Oa6&g4we$$=A5 zkV|ui*0{#^(%Pb)gwkgs3>9cEsXkd{w5$@Vnln%!;&AW%rll-q6t^I@MctebB44$u zU#;?rT=9c3(o2y%NG6tgSr$&JZM*VJV)6`NUCP+XH+F6o9DGE@0& z+|$e0f$@vV$jd2ZDZT$_dP!p{vD3MrhlfE^?W{t5hgw$6bPH@-9@qnfN7tno#} z!N!$DRQ(w|vncOAHDC$GT681Oi4@TL*q9~Cp07G`WCLJQo|ekTil}Rz^lrY$+ZtxaR*pl z0T07p#wP6h7EpUwrR^n^Z}Sl1f7EBu9cuHbupu66n}&{iwxV9B1>?`+tvv&s+dr!! z2Kne>1U75P^7cB1UC`dmj7?Qxo513MKG+3*x>(nrYyl%?8z6$!@`UkC6QV*MQeV>^-7dU7sr{ z!F=Xj?ev`TWd=jmvzu$uCMNvX`4{l~B^(`V7ds_Su0Su(0sux^Q#ilj%ZSQ}H)}8S z>3eZ0h>abGf}#Grc#e7(w{|1kwPYf0)o9mks;R_hb}n%}Z)bnV=@O}#PhxZgYnSuc zag=ttj%%5jeFkRtkQ#MR&4Imh%1jD!9YWA=yVy0rh`to{4NM?v@Q?n@r^Hh~dGb5~#Lf#Y(;zqcH zteVa4%&J>e8|n|X(lzBir^$%b2NZ?Z?ZB~Vr!9XKI^nF@>{TrGgVUR;+R3K=NY4Ji zKYc>f%Z!#8`Y_SbpdrM7mZH_>O6l$AhwHI&e^_aeS>5lcMi9h?b*E3VD7M&dbm<;G z1)(2DU=z6l81B$-50cjNW~r(u&sC4Rbi^et zx!W<|e8+4!LSxQYfaJ-g+|V*N`-z*BT9u(%XRunA*?0?Tn1&j5iHbiz$24}U7x`HF z_qsCUM#=jO8E^bjy8hZGQae32@}ZyOymCo=(o4dAZOlKTkxe@-rr5W?F55)gAMP{8 z=HKcpXYcHJQ&lo`FkI&uR79tTL4YSd6kW~rbMV5LQtt%KxL|T8ae@ENET9-GFZ^E- zu`&Zl2?~#G%mLB1t?EC>M~WkT{QV+(k7*a2#e5R%W!DuvCI!_`7|(unD}l zh9CM2_&p`8>9U!IPCIMVIy@U!Z#e`4vsTzBN-w~Z`6zvG01_nqZ`lqofNoy(5`m4N z9x>Nn{79y>#`3l4ZFgil=c88IVDb145bw}GGIZY*O_+3ww3;+^MMPuV?M}5B;&%Z- zEry)4?(|!N2%leid6BE5T8W(DjY9mIa4_0D0Gl(%^H<0fi$W>0E_`0Z?k?tLkj%8%up zcGFqP2=zV@0YLLP(if-np{uJhHn|0c+Nycv@!fFD#rtZ*d7d#6OUH*7mim zS&f{glXPUKXigu~i+SlCclD&CY~%#zkZ+Z0e)~**0#&4`D`d+-*)Tn`o-@Vb2b^$(sr9TA zf>@S_CDq0S#QZWWQg>8n`3Vbs0YT{%Q=fA9g9I@4f}D`(TjKRwJVmg?0R1`k~&=mwAoGJ&G)Q=mJ?T*}avA*K@VM{DKkMrlMmL|)z zdGy$y0B5es>bnsgu`yEO<0t`SQTC;vIKC1I+4_Ac?6n{729ik>lQRu5iNt#`p})1@ z8om`mlf}|YZAL_q*Z37 zPDc9BlH8{Oe`STsS>O**a!fT*eGJhY)ze;;ik!Od7kCWnY--a|saCr0fl1*%ze^y;^{clnAvncQ*2VR7gKkG45=_yw%YA zDm4sJI$$$SJ(xVVaDK42nx2aCCw@W{_>>6E=S+Mio!O}?QC2Jwp5Q$NtEvs3?0>zw_>9J~2`=kTkjdJuyS7oAP z5N?P)?l?YA%~W#I==ghA>uAl0CSm{97i>2SA$kE?!$E#LD0!%k!iIu}@7t!|j7Ry! zLcPb-9&$k9$Nq6Dy^UL3;O83K==gI+@AvtLADA)79Y7%5Y5@@E)$KGUzwQBn*Tf$K zM$?PTT(4^gt@M0AI;@7)-9RszDC=+`W`fcEn~->3tt;z$&mJm}Wi$_CYk*U)zIkxr zJ7Ig<@MwW_}ZsFs!syyiumHUmjsi%Q`UV%IE0Fu-8B$9S2$zi;V{n_pt ztg-fm^K|z)*-rv)fCRp#S91m60g#HJ$CmsknN0_0uZoVXL>RFI z{Q+-SjB_K4j&0)R7VjVFM!AIG{V`*=I5uAbGAt~Cya8p3-TWhY^I%oDte8IqWk>J59LL9Xy&$4j zDSb7Cct0lkrL>ggLS@=#4)oudxItet5yf|fa8_u>}# z-vEHpC;OM@v|_K-yNy`urM_~7TeGI^(*Z~PZI^l1CXVlSMh)wPv{x`KN$;cUAs6xr zjN`x*%L{6(yzLjY56+Hx;GJ%`mJ=FrKIpX&b10fqU?iY?4{&91;JPND#-c}gCFgH5 zxh~p7m$Cp&x6+DnI3iw$^2#}5aY|lQ9jafFwVcYtPy@(#$=P`odgocG#kMtJB|)nB zL0R!yVlf2d8L%4qN!n@BYD-HFEl++Fz85)x9*j}=fnlHd>|aJ)IC=g_SdO{6EL7)O z+33ij2zd=aGzi3Mi>juP%4arZkbJ|HC&8u0IW%$Y3+Lj|*17?nA5+ITd{`3%uq>hB zl!s;!1&Gsyud`5-?Nzc#fSYfQoVX5;5{2qtYkPvksJo773EoL#LcEbeT!Y@Z(^g(y zz5Nm>!swb-irIBW8}u!EgVRe|K%Xy=V3j?YbcO>h4SEFghp$M2O;d@uyXcS!0nKu@ zJFzz^V^-8*N_rSiar{HCXBvX)CHZE=nAfFu08&4vKe3sDH*GYbhubh_>(gc zhfveMt4{%uFoetroX$|AT$RNn5UdE)8-%x@)~R;ZAuWTSzfz?_F2JD#{bQd{h6# zA5i1*^nF;h8Bz}T{%w1eM_5sSVGb8kDm%AwdlkxIZ#2BJ+4qA+x9JbzhM;D znL0ZCDWv10fZHf;5!D;==H{irlb9pqPtCf zcNRVLfYfG>&_}K`L)zkAcuz@npL>Zozk5<0P>pAi3AO27cW5b|qR8u&w5q`V-IP?K zd|KcHvhHs9ihsIE0`WMg6N4$se-J-WG5M z$(B}wuLT+u{&xjT7oA`yQoxR^0Pj0eJ&3n{vx@dU)s{0irM(nYP|oOPh%T*0q)OXx zLVFC4z{Q5Lk*x~b&DdrqmyQ~g?wP`-?4_d;RlNa4f7wT>BHq6(Z1#CIF2Zrq6qP~F zn;xVdcCE<6BR>k7q%l29<7`?IR_8&MpqHhonDWy*i@-5{1{mr;lRn15YT7S30x3h= z#fag>M2vlAs7zXgXjqgY-L%>rC@AnS4g`P>hvzt4=L@gh?-{TtaBURN&b~3mLaO6| z%3CK~CjR+}mwo-g&8``pJ|ZzLw9N7bhE|7oB`v?&j1jugSgjOuD0pr*a;6);4Mv*Z3;vfQ_Lo2Ti9krWt03L6y)d8704fTGSnKHjD zUII+Ed3UMR15Dm1I^Y3(L}_GD58caUQj+*lUc5mhBDa4svwF^^Y*!V&s!VQihiYxa zSKsCJmWa^__j*=O_-XcLWwtU)vW44U<;Mi8{AjS5*Z47+Y@?8{L4J_!kP-Ooc$1Kb zxmD!>v1~GHSs9o?_Y={KaL@bzFP4JJG~W7J-balZUlxM!dzL>lFK2Qv;ERa=YBg)V zi3)v>N;u-cydg4j$QKC~JD@Cdm1D zz8kJVShLFkmObtOR^^YGXkvUuFj{^1qT?}%vMa5Xf6~^c@#*26w?|6)l6v-Zkn^hq#Z|*;OQ3Ic&FR465<5z9 z+{q}7>CTMD&~Nb;jQ(HHnKXP9JHOmZ1O{*=43cCs6_*Xc$3U@}H;S!$L;x0F)(n_D zKX)+o7vRp?nb+G`Yz5j|zh%~uZN1$!Q;v}BuWAo`R}QD|3O*NNeu?I+>LyEGaIsStA z1I=n%4e`O&Lk?GZpR#S}ewfw0VfwE9I7ll(E@~XJ+74+HNC5^t#S}E%?zXfB$`DK_ z8BtB>l&6M}_Jt(aBTM}J0KMVwI{wXjsBwYBrZ7iYBcj*d`VE$=#66L5c^*ek(OzMYs{%PmekOHg4-*MFmOsEfYwt z`}bkxAjPwnTmdZUp#$_EyCfoNrOKaN_y0*{o6%yrI12(Lac}KxO9#-}k`d2uXkSod zxqktqm99{8{fuU1zO}_D2N0VI74v~&%SY_OR&)T>ZN{1cl5t+|t#Y0t8m^ZLv^WP% z*vyQ^P&eLLvYDEuF&%rgQI56CqD~&u0y#-mDnm~Sqv4mWA_9?h|7cu{<6{A;ZiYSW zPl9QgN+Whzf)r}|t3c2(U&||bLX}e$`hQN3%|tsou1ZFP4`udVIdSV^?>uD-NDyfp zFee^l+2BPI=qrHUL0;6e3)cWO+i2gY`ykS!3}|IhxJ0A^;lk4-`EFu=+L7Z-UpYy(Z!yfGj)7MgrwzVwQF> zx0~Pw&5}nY@LrfB9F7<|g7JCO=g`0q^!8|83;-_93vXE`DDNd*e#ECWe8e@~`+Od? z1;DmM{C87}*RFYcgd9ZBGA96e79G(I86;07kjIlwGjR4fq|@QCg=L zz)dw`&E4Zo49}}eX&uGf17w7qs*Kq+)Id>dGHEL7-aBd8qp69dF0X;1uM1*6|LPcT z`{C*V20%rR>p{cHfa?DMlKrZZmUE{7xjoj@pmK=3xAKD@u0K$+rKoOhj1!|;^7ri7 z_kp5xYUNgA+88-M7pqkw*vX~to$5oXU@nv!N#6@I< zO*XB+xYscYu6o!407Ot$P}1{)iI9LI!dbUnS_&ucN1qp^QVGRRdLM*aMtT5>59Deo zh=iaX@QWqg)pi-EDf>>M>-=1mYASt4f$~a#2~^SrxTzTcBDMaxe^(iG18Uk3NS^ze z;Ia`kKdKRfiZ9+e#|+SNA7BD!*Zz=s!q=3vpV!bO$|uQiAMpJpO`w_7elgMFsPhcE z)__=*o(su)ARO_@5U)>pJ%<|vVz8Ft@$e1+%#5JrzY*7E1G7LsJZ6*grQUD2qY zGh%uH-f2Cqp>9Rf3XXHexgbJ(!N|G-SZu7m)1_uDHT%hJTKOr0IC-)7qwMnbF8)7& z4Co%UDZxyDXbET*iZ=>?DM~}}3slt^FYpw5?Z(MHwads;pyFi~DtxSdG1&YQsg;t$ zgRBxT3Pl(hZs6u(sKcybicpNF!d56lM)?Qfza4M;u8>@*oho7D*F?qRspw_i7o700 zwx#BU8z7!*RTi>ojpAW?$m@XgaVE_%5nv(d4S`(}{?Mqid|M*q`(X7K9PlK%x!LA( z>)xlAXU}^0!)V6f$wrYSQ4dn0xr2r^PJ)M*nQScM03VOosNCc)82fY!dJ&}9z~93N zxVziU9$5RRcEDuh_svy9oSw1++*N(Y71cc-+T8W207zLsr#yR-S}L$qX(H+O)9?%) z?XL2xY#z)bsCFOLoV_dnKm1v`M3rc-bhz*xBu5D4o*#3%rKvj@qT+(G-|C~8o9{Ym zl(l~1Ph|kPt$QfnLha=Go#vU%z?_@Zk4MdoIIL(yXFscT)HTLB(;9yeqUK(^F~f(l z4q*HE^;^dZr5VQWtgVa(e8*3(9-K*cT)?q;k0;7A+}rydApH#M7fVXU5zGEZ*5j0V z%rFqcE45sUql~YUs!|VwfxIK3*+1vQI;kLIW11zK-wPKE;p-4Pw=vZc55cX7BBYfk zN~Fn<&c}1#Oc_72OV$FIKwPij^S*Cr6a1ROV4%vck@JVS>?^A|?(FPABsl&qkMj3; z3cT^QJEWK1|CJGKvvLe<2b59jN`Oj#9B`2YapZ#EufXB9vSg1hzy+C*dXg>-D5UD( ztmEbzMf}g501CqYvvKi^(sMC>8npa1NUNS)hg5$Mh@W&!El24v^j6=qQE8(@OZcY#}L&ceVo wHM#!)9OXIw!}-6KH*>ze4)-gb`rU!^lu@Bjb+ literal 0 HcmV?d00001 diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg new file mode 100644 index 0000000..acb0503 --- /dev/null +++ b/docs/assets/logo.svg @@ -0,0 +1,71 @@ + + + +image/svg+xml + + + + + + + + + + \ No newline at end of file diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 0000000..5829909 --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,9 @@ +:root > * { + --md-primary-fg-color: #009DE1; + --md-primary-fg-color--light: #009DE1; + --md-primary-fg-color--dark: #009DE1; +} + +.md-nav__title { + display: none; +} \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index e474f18..46dace4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,6 +1,6 @@ site_name: pyharp repo_url: "https://github.com/fchampalimaud/pyharp" -repo_name: "pyharp" +# repo_name: "pyharp" plugins: - search @@ -29,6 +29,13 @@ markdown_extensions: - pymdownx.emoji: emoji_index: !!python/name:material.extensions.emoji.twemoji emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences - toc: permalink: "#" @@ -36,26 +43,46 @@ theme: name: material icon: repo: fontawesome/brands/github + logo: assets/logo.svg + favicon: assets/favicon.png features: - content.tooltips - toc.follow + - content.code.copy + # - navigation.sections + - navigation.indexes + - navigation.expand palette: - - media: "(prefers-color-scheme)" - toggle: - icon: material/brightness-auto - name: Switch to light mode - - media: "(prefers-color-scheme: light)" - scheme: default - primary: blue - accent: blue - toggle: - icon: material/weather-sunny - name: Switch to dark mode - - media: "(prefers-color-scheme: dark)" - scheme: slate - primary: red - accent: red - toggle: - icon: material/weather-night - name: Switch to system preference + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Switch to light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: custom + accent: light-blue + toggle: + icon: material/weather-sunny + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: custom + accent: light-blue + toggle: + icon: material/weather-night + name: Switch to system preference + +nav: + - Home: index.md + - Examples: + - examples/index.md + - Getting Device Info: examples/get_info.md + - Check Device ID: examples/check_device_id.md + - Wait for Events: examples/wait_for_events.md + - Write and Read from Registers: examples/write_and_read_from_registers.md + - API: + - Device: api/device.md + - Messages: api/messages.md +extra_css: +- stylesheets/extra.css \ No newline at end of file From 53b41a4f84e56f9f1f27e1cf8091602f05027fc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Grilo?= Date: Fri, 4 Apr 2025 18:05:47 +0100 Subject: [PATCH 068/267] Docs: modify home page --- docs/index.md | 70 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/docs/index.md b/docs/index.md index f48b45f..335330f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,68 @@ -# Introduction +# pyharp -!!! Warning - Work in Progress! \ No newline at end of file +Python implementation of the Harp protocol for hardware control and data acquisition. + +## Installation + +```bash +uv add pyharp +# or +pip install pyharp +``` + +## Quick Start + +```python +from pyharp.device import Device + +# Connect to a device +device = Device("/dev/ttyUSB0") + +# Get device information +device.info() + +# define register_address +register_address = 32 + +# Read from register +value = device.read_u8(register_address) + +# Write to register +device.write_u8(register_address, value) + +# Disconnect when done +device.disconnect() +``` + +or using the `with` statement: + +```python +from pyharp.device import Device + +with Device("/dev/ttyUSB0") as device: + # Get device information + device.info() + + # define register_address + register_address = 32 + + # Read from register + value = device.read_u8(register_address) + + # Write to register + device.write_u8(register_address, value) +``` + +## for Linux + +### Install UDEV Rules + +Install by either copying `10-harp.rules` over to your `/etc/udev/rules.d` folder or by symlinking it with: +```` +sudo ln -s /absolute/path/to/10-harp.rules /etc/udev/rules.d/10-harp.rules +```` + +Then reload udev rules with +```` +sudo udevadm control --reload-rules +```` From b80d79bb6590843ecf99330067959d38f219e879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Grilo?= Date: Fri, 4 Apr 2025 18:07:27 +0100 Subject: [PATCH 069/267] Test: comment incorrect tests to fix in the future --- tests/test_device.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/tests/test_device.py b/tests/test_device.py index a5a8e6f..00d9d3b 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -5,14 +5,14 @@ DEFAULT_ADDRESS = 42 - -def test_create_device() -> None: - # open serial connection and load info - device = Device("/dev/ttyUSB0", "dump.bin") - assert device._ser.is_open - device.info() - device.disconnect() - assert not device._ser.is_open +# FIXME +# def test_create_device() -> None: +# # open serial connection and load info +# device = Device("COM74", "dump.bin") +# assert device._ser.is_open +# device.info() +# device.disconnect() +# assert not device._ser.is_open def test_read_U8() -> None: @@ -82,9 +82,10 @@ def test_U8() -> None: # # assert data[0] == '\t' -def test_device_events(device: Device) -> None: - while True: - print(device.event_count()) - for msg in device.get_events(): - print(msg) - time.sleep(0.3) +# FIXME +# def test_device_events(device: Device) -> None: +# while True: +# print(device.event_count()) +# for msg in device.get_events(): +# print(msg) +# time.sleep(0.3) From d87c1ae8b2bacb432784a8be7862eaef27141651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Grilo?= Date: Fri, 4 Apr 2025 18:14:34 +0100 Subject: [PATCH 070/267] Docs: add documentation to HarpMessage and Device classes --- pyharp/device.py | 549 ++++++++++++++++++++++++++++++++++++++++++--- pyharp/messages.py | 104 ++++++++- 2 files changed, 620 insertions(+), 33 deletions(-) diff --git a/pyharp/device.py b/pyharp/device.py index 0ea24d4..f572781 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -23,12 +23,34 @@ class DeviceMode(Enum): class Device: """ - https://github.com/harp-tech/protocol/blob/master/Device%201.1%201.0%2020220402.pdf + The `Device` class provides the interface for interacting with Harp devices. This implementation of the Harp device was based on the official documentation available on the [harp-tech website](https://harp-tech.org/protocol/Device.html). + + Attributes + ---------- + WHO_AM_I : int + the device ID number. A list of devices can be found [here](https://github.com/harp-tech/protocol/blob/main/whoami.md) + DEFAULT_DEVICE_NAME : str + the device name, i.e. "Behavior". This name is derived by cross-referencing the `WHO_AM_I` identifier with the corresponding device name in the `device_names` dictionary + HW_VERSION_H : int + the major hardware version + HW_VERSION_L : int + the minor hardware version + ASSEMBLY_VERSION : int + the version of the assembled components + HARP_VERSION_H : int + the major Harp core version + HARP_VERSION_L : int + the minor Harp core version + FIRMWARE_VERSION_H : int + the major firmware version + FIRMWARE_VERSION_L : int + the minor firmware version + DEVICE_NAME : str + the device name stored in the Harp device + SERIAL_NUMBER : int, optional + the serial number of the device """ - _ser: HarpSerial - _dump_file_path: Path - WHO_AM_I: int DEFAULT_DEVICE_NAME: str HW_VERSION_H: int @@ -49,6 +71,16 @@ def __init__( dump_file_path: Optional[str] = None, read_timeout_s=1, ): + """ + Parameters + ---------- + serial_port : str + the serial port used to establish the connection with the Harp device. It must be denoted as `ttyUSBx` in Linux and `COMx` in Windows, where `x` is the number of the serial port + dump_file_path: str, optional + the binary file to which all Harp messages will be written + read_timeout_s: float, optional + _TODO_ + """ self.log = logging.getLogger(f"{__name__}.{self.__class__.__name__}") self._serial_port = serial_port if dump_file_path is None: @@ -60,6 +92,9 @@ def __init__( self.load() def load(self) -> None: + """ + Loads the data stored in the device's common registers. + """ self.WHO_AM_I = self._read_who_am_i() self.DEFAULT_DEVICE_NAME = self._read_default_device_name() self.HW_VERSION_H = self._read_hw_version_h() @@ -73,6 +108,9 @@ def load(self) -> None: self.SERIAL_NUMBER = self._read_serial_number() def info(self) -> None: + """ + Prints the device information. + """ print("Device info:") print(f"* Who am I: ({self.WHO_AM_I}) {self.DEFAULT_DEVICE_NAME}") print(f"* HW version: {self.HW_VERSION_H}.{self.HW_VERSION_L}") @@ -84,6 +122,9 @@ def info(self) -> None: print(f"* Mode: {self.read_device_mode().name}") def connect(self) -> None: + """ + Connects to the Harp device. + """ self._ser = HarpSerial( self._serial_port, # "/dev/tty.usbserial-A106C8O9" baudrate=1000000, @@ -95,16 +136,33 @@ def connect(self) -> None: ) def disconnect(self) -> None: + """ + Disconnects from the Harp device. + """ self._ser.close() - def read_device_mode(self) -> DeviceMode: + def _read_device_mode(self) -> DeviceMode: + """ + Reads the current operation mode of the Harp device. + + Returns + ------- + DeviceMode + the current device mode + """ address = CommonRegisters.OPERATION_CTRL reply = self.read_u8(address) return DeviceMode(reply.payload_as_int() & 0x03) def dump_registers(self) -> list: - """Assert the DUMP bit to dump the values of all core and app registers - as Harp Read Reply Messages. + """ + Asserts the DUMP bit to dump the values of all core and app registers + as Harp Read Reply Messages. More information on the DUMP bit can be found [here](https://harp-tech.org/protocol/Device.html#r_operation_ctrl-u16--operation-mode-configuration). + + Returns + ------- + list + the list containing the reply Harp messages for all the device's registers """ address = CommonRegisters.OPERATION_CTRL reg_value = self.read_u8(address).payload_as_int() @@ -119,58 +177,114 @@ def dump_registers(self) -> list: break return replies -# TODO: Not sure if we want to implement these. Delete if no. def set_mode(self, mode: DeviceMode) -> ReplyHarpMessage: - """Change the device's OPMODE. Reply can be ignored.""" + """ + Sets the operation mode of the device. + + Parameters + ---------- + mode : DeviceMode + the new device mode value + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message + """ address = CommonRegisters.OPERATION_CTRL - # Read register first. + + # Read register first reg_value = self.read_u8(address).payload_as_int() reg_value &= ~0x03 # mask off old mode. reg_value |= mode.value reply = self.send(WriteHarpMessage(PayloadType.U8, address, reg_value).frame) return reply - def enable_status_led(self): - """enable the device's status led if one exists.""" + def enable_status_led(self) -> ReplyHarpMessage: + """ + Enables the device's status led. + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message + """ address = CommonRegisters.OPERATION_CTRL - # Read register first. + + # Read register first reg_value = self.read_u8(address).payload_as_int() reg_value |= (1 << 5) reply = self.send(WriteHarpMessage(PayloadType.U8, address, reg_value).frame) - def disable_status_led(self): - """disable the device's status led if one exists.""" + def disable_status_led(self) -> ReplyHarpMessage: + """ + Disables the device's status led. + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message + """ address = CommonRegisters.OPERATION_CTRL - # Read register first. + + # Read register first reg_value = self.read_u8(address).payload_as_int() reg_value &= ~(1 << 5) reply = self.send(WriteHarpMessage(PayloadType.U8, address, reg_value).frame) - def enable_alive_en(self): - """Enable ALIVE_EN such that the device sends an event each second.""" + def enable_alive_en(self) -> ReplyHarpMessage: + """ + Enables the ALIVE_EN bit so that the device sends an event each second. More information on the ALIVE_EN bit can be found [here](https://harp-tech.org/protocol/Device.html#r_operation_ctrl-u16--operation-mode-configuration). + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message + """ address = CommonRegisters.OPERATION_CTRL - # Read register first. + + # Read register first reg_value = self.read_u8(address).payload_as_int() reg_value |= (1 << 7) reply = self.send(WriteHarpMessage(PayloadType.U8, address, reg_value).frame) - def disable_alive_en(self): - """disable ALIVE_EN such that the device does not send an event each second.""" + def disable_alive_en(self) -> ReplyHarpMessage: + """ + Disables the ALIVE_EN bit so that the device does not send an event each second. More information on the ALIVE_EN bit can be found [here](https://harp-tech.org/protocol/Device.html#r_operation_ctrl-u16--operation-mode-configuration). + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message + """ address = CommonRegisters.OPERATION_CTRL - # Read register first. + + # Read register first reg_value = self.read_u8(address).payload[0] reg_value &= (1 << 7) ^ 0xFF # bitwise ~ operator substitute for Python ints. reply = self.send(WriteHarpMessage(PayloadType.U8, address, reg_value).frame) def reset_device(self): address = CommonRegisters.RESET_DEV - # reset_value = 0xFF & (1< ReplyHarpMessage: - """Send a harp message; return the device's reply.""" - #print(f"Sending: {repr(message_bytes)}") + """ + Sends a Harp message. + + Parameters + ---------- + message_bytes : bytearray + the bytearray containing the message to be sent to the device + dump : bool, optional + indicates whether the reply message should be dumped or not + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message + """ self._ser.write(message_bytes) # TODO: handle case where read is None @@ -182,19 +296,37 @@ def send(self, message_bytes: bytearray, dump: bool = True) -> ReplyHarpMessage: return reply def _read(self) -> Union[ReplyHarpMessage, None]: - """(Blocking) Read an incoming serial message.""" + """ + Reads an incoming serial message in a blocking way. + + Returns + ------- + Union[ReplyHarpMessage, None] + the incoming Harp message in case it exists + """ try: return self._ser.msg_q.get(block=True, timeout=self.read_timeout_s) except queue.Empty: return None - + def _dump_reply(self, reply: bytes): + """ + Dumps the reply to a Harp message in the dump file in case it exists. + """ + # TODO: try to handle a None _dump_file_path in a different way assert self._dump_file_path is not None with self._dump_file_path.open(mode="ab") as f: f.write(reply) def get_events(self) -> list[ReplyHarpMessage]: - """Get all events from the event queue.""" + """ + Gets all events from the event queue. + + Returns + ------- + list + the list containing every Harp event message that were on the queue + """ msgs = [] while True: try: @@ -204,50 +336,192 @@ def get_events(self) -> list[ReplyHarpMessage]: return msgs def event_count(self) -> int: - """Get the number of events in the event queue.""" + """ + Gets the number of events in the event queue. + + Returns + ------- + int + the number of events in the event queue + """ return self._ser.event_q.qsize() def read_u8(self, address: int, dump: bool = True) -> ReplyHarpMessage: + """ + Reads the value of a register of type U8. + + Parameters + ---------- + address : int + the register to be read + dump : bool, optional + indicates whether the reply message should be dumped or not + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message that will contain the value read from the register + """ return self.send( ReadHarpMessage(payload_type=PayloadType.U8, address=address).frame, dump ) def read_s8(self, address: int, dump: bool = True) -> ReplyHarpMessage: + """ + Reads the value of a register of type S8. + + Parameters + ---------- + address : int + the register to be read + dump : bool, optional + indicates whether the reply message should be dumped or not + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message that will contain the value read from the register + """ return self.send( ReadHarpMessage(payload_type=PayloadType.S8, address=address).frame, dump ) def read_u16(self, address: int, dump: bool = True) -> ReplyHarpMessage: + """ + Reads the value of a register of type U16. + + Parameters + ---------- + address : int + the register to be read + dump : bool, optional + indicates whether the reply message should be dumped or not + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message that will contain the value read from the register + """ return self.send( ReadHarpMessage(payload_type=PayloadType.U16, address=address).frame, dump ) def read_s16(self, address: int, dump: bool = True) -> ReplyHarpMessage: + """ + Reads the value of a register of type S16. + + Parameters + ---------- + address : int + the register to be read + dump : bool, optional + indicates whether the reply message should be dumped or not + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message that will contain the value read from the register + """ return self.send( ReadHarpMessage(payload_type=PayloadType.S16, address=address).frame, dump ) def read_u32(self, address: int, dump: bool = True) -> ReplyHarpMessage: + """ + Reads the value of a register of type U32. + + Parameters + ---------- + address : int + the register to be read + dump : bool, optional + indicates whether the reply message should be dumped or not + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message that will contain the value read from the register + """ return self.send( ReadHarpMessage(payload_type=PayloadType.U32, address=address).frame, dump ) def read_s32(self, address: int, dump: bool = True) -> ReplyHarpMessage: + """ + Reads the value of a register of type S32. + + Parameters + ---------- + address : int + the register to be read + dump : bool, optional + indicates whether the reply message should be dumped or not + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message that will contain the value read from the register + """ return self.send( ReadHarpMessage(payload_type=PayloadType.S32, address=address).frame, dump ) def read_u64(self, address: int, dump: bool = True) -> ReplyHarpMessage: + """ + Reads the value of a register of type U64. + + Parameters + ---------- + address : int + the register to be read + dump : bool, optional + indicates whether the reply message should be dumped or not + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message that will contain the value read from the register + """ return self.send( ReadHarpMessage(payload_type=PayloadType.U64, address=address).frame, dump ) def read_s64(self, address: int, dump: bool = True) -> ReplyHarpMessage: + """ + Reads the value of a register of type S64. + + Parameters + ---------- + address : int + the register to be read + dump : bool, optional + indicates whether the reply message should be dumped or not + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message that will contain the value read from the register + """ return self.send( ReadHarpMessage(payload_type=PayloadType.S64, address=address).frame, dump ) def read_float(self, address: int, dump: bool = True) -> ReplyHarpMessage: + """ + Reads the value of a register of type Float. + + Parameters + ---------- + address : int + the register to be read + dump : bool, optional + indicates whether the reply message should be dumped or not + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message that will contain the value read from the register + """ return self.send( ReadHarpMessage(payload_type=PayloadType.Float, address=address).frame, dump ) @@ -255,6 +529,23 @@ def read_float(self, address: int, dump: bool = True) -> ReplyHarpMessage: def write_u8( self, address: int, value: int | List[int], dump: bool = True ) -> ReplyHarpMessage: + """ + Writes the value of a register of type U8. + + Parameters + ---------- + address : int + the register to be written on + value: int | List[int] + the value to be written to the register + dump : bool, optional + indicates whether the reply message should be dumped or not + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message + """ return self.send( WriteHarpMessage( payload_type=PayloadType.U8, @@ -267,6 +558,23 @@ def write_u8( def write_s8( self, address: int, value: int | List[int], dump: bool = True ) -> ReplyHarpMessage: + """ + Writes the value of a register of type S8. + + Parameters + ---------- + address : int + the register to be written on + value: int | List[int] + the value to be written to the register + dump : bool, optional + indicates whether the reply message should be dumped or not + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message + """ return self.send( WriteHarpMessage( payload_type=PayloadType.S8, @@ -279,6 +587,23 @@ def write_s8( def write_u16( self, address: int, value: int | List[int], dump: bool = True ) -> ReplyHarpMessage: + """ + Writes the value of a register of type U16. + + Parameters + ---------- + address : int + the register to be written on + value: int | List[int] + the value to be written to the register + dump : bool, optional + indicates whether the reply message should be dumped or not + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message + """ return self.send( WriteHarpMessage( payload_type=PayloadType.U16, @@ -291,6 +616,23 @@ def write_u16( def write_s16( self, address: int, value: int | List[int], dump: bool = True ) -> ReplyHarpMessage: + """ + Writes the value of a register of type S16. + + Parameters + ---------- + address : int + the register to be written on + value: int | List[int] + the value to be written to the register + dump : bool, optional + indicates whether the reply message should be dumped or not + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message + """ return self.send( WriteHarpMessage( payload_type=PayloadType.S16, @@ -303,6 +645,23 @@ def write_s16( def write_u32( self, address: int, value: int | List[int], dump: bool = True ) -> ReplyHarpMessage: + """ + Writes the value of a register of type U32. + + Parameters + ---------- + address : int + the register to be written on + value: int | List[int] + the value to be written to the register + dump : bool, optional + indicates whether the reply message should be dumped or not + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message + """ return self.send( WriteHarpMessage( payload_type=PayloadType.U32, @@ -315,6 +674,23 @@ def write_u32( def write_s32( self, address: int, value: int | List[int], dump: bool = True ) -> ReplyHarpMessage: + """ + Writes the value of a register of type S32. + + Parameters + ---------- + address : int + the register to be written on + value: int | List[int] + the value to be written to the register + dump : bool, optional + indicates whether the reply message should be dumped or not + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message + """ return self.send( WriteHarpMessage( payload_type=PayloadType.S32, @@ -327,6 +703,23 @@ def write_s32( def write_u64( self, address: int, value: int | List[int], dump: bool = True ) -> ReplyHarpMessage: + """ + Writes the value of a register of type U64. + + Parameters + ---------- + address : int + the register to be written on + value: int | List[int] + the value to be written to the register + dump : bool, optional + indicates whether the reply message should be dumped or not + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message + """ return self.send( WriteHarpMessage( payload_type=PayloadType.U64, @@ -339,6 +732,23 @@ def write_u64( def write_s64( self, address: int, value: int | List[int], dump: bool = True ) -> ReplyHarpMessage: + """ + Writes the value of a register of type S64. + + Parameters + ---------- + address : int + the register to be written on + value: int | List[int] + the value to be written to the register + dump : bool, optional + indicates whether the reply message should be dumped or not + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message + """ return self.send( WriteHarpMessage( payload_type=PayloadType.S64, @@ -351,6 +761,23 @@ def write_s64( def write_float( self, address: int, value: float | List[float], dump: bool = True ) -> ReplyHarpMessage: + """ + Writes the value of a register of type Float. + + Parameters + ---------- + address : int + the register to be written on + value: int | List[int] + the value to be written to the register + dump : bool, optional + indicates whether the reply message should be dumped or not + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message + """ return self.send( WriteHarpMessage( payload_type=PayloadType.Float, @@ -361,6 +788,14 @@ def write_float( ) def _read_who_am_i(self) -> int: + """ + Reads the value stored in the `WHO_AM_I` register. + + Returns + ------- + int + the value of the `WHO_AM_I` register. + """ address = CommonRegisters.WHO_AM_I reply: ReplyHarpMessage = self.read_u16(address, dump=False) @@ -368,9 +803,25 @@ def _read_who_am_i(self) -> int: return reply.payload_as_int() def _read_default_device_name(self) -> str: + """ + Returns the `DEFAULT_DEVICE_NAME` by cross-referencing the `WHO_AM_I` with the corresponding device name in the `device_names` dictionary. + + Returns + ------- + str + the default device name. + """ return device_names.get(self.WHO_AM_I, "Unknown device") def _read_hw_version_h(self) -> int: + """ + Reads the value stored in the `HW_VERSION_H` register. + + Returns + ------- + int + the value of the `HW_VERSION_H` register. + """ address = CommonRegisters.HW_VERSION_H reply: ReplyHarpMessage = self.read_u8(address, dump=False) @@ -378,6 +829,14 @@ def _read_hw_version_h(self) -> int: return reply.payload_as_int() def _read_hw_version_l(self) -> int: + """ + Reads the value stored in the `HW_VERSION_L` register. + + Returns + ------- + int + the value of the `HW_VERSION_L` register. + """ address = CommonRegisters.HW_VERSION_L reply: ReplyHarpMessage = self.read_u8(address, dump=False) @@ -385,6 +844,14 @@ def _read_hw_version_l(self) -> int: return reply.payload_as_int() def _read_assembly_version(self) -> int: + """ + Reads the value stored in the `ASSEMBLY_VERSION` register. + + Returns + ------- + int + the value of the `ASSEMBLY_VERSION` register. + """ address = CommonRegisters.ASSEMBLY_VERSION reply: ReplyHarpMessage = self.read_u8(address, dump=False) @@ -420,6 +887,14 @@ def _read_fw_l_version(self) -> int: return reply.payload_as_int() def _read_device_name(self) -> str: + """ + Reads the value stored in the `DEVICE_NAME` register. + + Returns + ------- + int + the value of the `DEVICE_NAME` register. + """ address = CommonRegisters.DEVICE_NAME reply: ReplyHarpMessage = self.read_u8(address, dump=False) @@ -427,6 +902,14 @@ def _read_device_name(self) -> str: return reply.payload_as_string() def _read_serial_number(self) -> int: + """ + Reads the value stored in the `SERIAL_NUMBER` register. + + Returns + ------- + int + the value of the `SERIAL_NUMBER` register. + """ address = CommonRegisters.SERIAL_NUMBER reply: ReplyHarpMessage = self.read_u8(address, dump=False) @@ -437,7 +920,8 @@ def _read_serial_number(self) -> int: return reply.payload_as_int() def __enter__(self): - """Support for using Device with 'with' statement. + """ + Support for using Device with 'with' statement. Returns ------- @@ -449,7 +933,8 @@ def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): - """Cleanup resources when exiting the 'with' block. + """ + Cleanup resources when exiting the 'with' block. Parameters ---------- @@ -462,4 +947,4 @@ def __exit__(self, exc_type, exc_val, exc_tb): """ self.disconnect() # Return False to propagate exceptions that occurred in the with block - return False \ No newline at end of file + return False diff --git a/pyharp/messages.py b/pyharp/messages.py index 2c8e780..2b5aa46 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -8,7 +8,32 @@ class HarpMessage: """ - https://github.com/harp-tech/protocol/blob/main/BinaryProtocol-8bit.md + The `HarpMessage` class implements the Harp message as described in the [protocol](https://harp-tech.org/protocol/BinaryProtocol-8bit.html). + + Attributes + ---------- + frame : bytearray + the bytearray containing the whole Harp message + message_type : MessageType + the message type + length : int + the length parameter of the Harp message + """ + Calculates the checksum of the Harp message. + + Returns + ------- + int + the value of the checksum + """ + address : int + the address of the register to which the Harp message refers to + port : int + indicates the origin or destination of the Harp message in case the device is a hub of Harp devices. The value 255 points to the device itself (default value). + payload_type : PayloadType + the payload type + checksum : int + the sum of all bytes contained in the Harp message """ DEFAULT_PORT: int = 255 @@ -18,6 +43,14 @@ def __init__(self): self._frame = bytearray() def calculate_checksum(self) -> int: + """ + Calculates the checksum of the Harp message. + + Returns + ------- + int + the value of the checksum + """ checksum: int = 0 for i in self.frame: checksum += i @@ -25,34 +58,103 @@ def calculate_checksum(self) -> int: @property def frame(self) -> bytearray: + """ + The bytearray containing the whole Harp message. + + Returns + ------- + bytearray + the bytearray containing the whole Harp message + """ return self._frame @property def message_type(self) -> MessageType: + """ + The message type. + + Returns + ------- + MessageType + the message type + """ return MessageType(self._frame[0]) @property def length(self) -> int: + """ + The length parameter of the Harp message. + + Returns + ------- + int + the length parameter of the Harp message + """ return self._frame[1] @property def address(self) -> int: + """ + The address of the register to which the Harp message refers to. + + Returns + ------- + int + the address of the register to which the Harp message refers to + """ return self._frame[2] @property def port(self) -> int: + """ + Indicates the origin or destination of the Harp message in case the device is a hub of Harp devices. The value 255 points to the device itself (default value). + + Returns + ------- + int + the port value + """ return self._frame[3] @property def payload_type(self) -> PayloadType: + """ + The payload type. + + Returns + ------- + PayloadType + the payload type + """ return PayloadType(self._frame[4]) @property def checksum(self) -> int: + """ + The sum of all bytes contained in the Harp message. + + Returns + ------- + int + the sum of all bytes contained in the Harp message + """ return self._frame[-1] @staticmethod def parse(frame: bytearray) -> ReplyHarpMessage: + """ + Parses a bytearray to a (reply) Harp message. + + Parameters + ---------- + frame : bytearray + the bytearray will be parsed into a (reply) Harp message + + Returns + ------- + ReplyHarpMessage + the Harp message object parsed from the original bytearray + """ return ReplyHarpMessage(frame) From 56b9df37bc1a3b5c143f5e54b0e6ff0d03cf3ee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Grilo?= Date: Fri, 4 Apr 2025 18:18:17 +0100 Subject: [PATCH 071/267] Refactor: change base.py enums to IntEnums --- pyharp/base.py | 113 +++++++++++++++++++++++++++++++++++++++++---- pyharp/messages.py | 26 ++++------- 2 files changed, 112 insertions(+), 27 deletions(-) diff --git a/pyharp/base.py b/pyharp/base.py index af4219f..f367820 100644 --- a/pyharp/base.py +++ b/pyharp/base.py @@ -1,12 +1,30 @@ -from enum import Enum +from enum import IntEnum -# TODO: Find a way to really hide this from the user +# Bit masks for the PayloadType _isUnsigned: int = 0x00 _isSigned: int = 0x80 _isFloat: int = 0x40 _hasTimestamp: int = 0x10 -class MessageType(Enum): + +class MessageType(IntEnum): + """ + An enumeration of the allowed message types of a Harp message. More information on the MessageType byte of a Harp message can be found [here](https://harp-tech.org/protocol/BinaryProtocol-8bit.html#messagetype-1-byte). + + Attributes + ---------- + READ : int + the value that corresponds to a Read Harp message (1) + WRITE : int + the value that corresponds to a Write Harp message (2) + EVENT : int + the value that corresponds to an Event Harp message (3). Messages of this type are only meant to be send by the device + READ_ERROR : int + the value that corresponds to a Read Error Harp message (9). Messages of this type are only meant to be send by the device + WRITE_ERROR : int + the value that corresponds to a Write Error Harp message (10). Messages of this type are only meant to be send by the device + """ + READ: int = 1 WRITE: int = 2 EVENT: int = 3 @@ -14,17 +32,59 @@ class MessageType(Enum): WRITE_ERROR: int = 10 -class PayloadType(Enum): - U8 = _isUnsigned | 1 # 1 - S8 = _isSigned | 1 # 129 - U16 = _isUnsigned | 2 # 2 - S16 = _isSigned | 2 # 130 +class PayloadType(IntEnum): + """ + An enumeration of the allowed payload types of a Harp message. More information on the PayloadType byte of a Harp message can be found [here](https://harp-tech.org/protocol/BinaryProtocol-8bit.html#payloadtype-1-byte). + + Attributes + ---------- + U8 : PayloadType + the value that corresponds to a message of type U8 + S8 : PayloadType + the value that corresponds to a message of type S8 + U16 : PayloadType + the value that corresponds to a message of type U16 + S16 : PayloadType + the value that corresponds to a message of type S16 + U32 : PayloadType + the value that corresponds to a message of type U32 + S32 : PayloadType + the value that corresponds to a message of type S32 + U64 : PayloadType + the value that corresponds to a message of type U64 + S64 : PayloadType + the value that corresponds to a message of type S64 + Float : PayloadType + the value that corresponds to a message of type Float + TimestampedU8 : PayloadType + the value that corresponds to a message of type TimestampedU8 + TimestampedS8 : PayloadType + the value that corresponds to a message of type TimestampedS8 + TimestampedU16 : PayloadType + the value that corresponds to a message of type TimestampedU16 + TimestampedS16 : PayloadType + the value that corresponds to a message of type TimestampedS16 + TimestampedU32 : PayloadType + the value that corresponds to a message of type TimestampedU32 + TimestampedS32 : PayloadType + the value that corresponds to a message of type TimestampedS32 + TimestampedU64 : PayloadType + the value that corresponds to a message of type TimestampedU64 + TimestampedS64 : PayloadType + the value that corresponds to a message of type TimestampedS64 + TimestampedFloat : PayloadType + the value that corresponds to a message of type TimestampedFloat + """ + + U8 = _isUnsigned | 1 + S8 = _isSigned | 1 + U16 = _isUnsigned | 2 + S16 = _isSigned | 2 U32 = _isUnsigned | 4 S32 = _isSigned | 4 U64 = _isUnsigned | 8 S64 = _isSigned | 8 Float = _isFloat | 4 - Timestamp = _hasTimestamp TimestampedU8 = _hasTimestamp | U8 TimestampedS8 = _hasTimestamp | S8 TimestampedU16 = _hasTimestamp | U16 @@ -36,7 +96,40 @@ class PayloadType(Enum): TimestampedFloat = _hasTimestamp | Float -class CommonRegisters: +class CommonRegisters(IntEnum): + """ + An enumeration with the registers that are common to every Harp device. More information on the common registers can be found [here](https://harp-tech.org/protocol/Device.html#table---list-of-available-common-registers). + + WHO_AM_I : int + the number of the `WHO_AM_I` register + HW_VERSION_H : int + the number of the `HW_VERSION_H` register + HW_VERSION_L : int + the number of the `HW_VERSION_L` register + ASSEMBLY_VERSION : int + the number of the `ASSEMBLY_VERSION` register + HARP_VERSION_H : int + the number of the `HARP_VERSION_H` register + HARP_VERSION_L : int + the number of the `HARP_VERSION_L` register + FIRMWARE_VERSION_H : int + the number of the `FIRMWARE_VERSION_H` register + FIRMWARE_VERSION_L : int + the number of the `FIRMWARE_VERSION_L` register + TIMESTAMP_SECOND : int + the number of the `TIMESTAMP_SECOND` register + TIMESTAMP_MICRO : int + the number of the `TIMESTAMP_MICRO` register + OPERATION_CTRL : int + the number of the `OPERATION_CTRL` register + RESET_DEV : int + the number of the `RESET_DEV` register + DEVICE_NAME : int + the number of the `DEVICE_NAME` register + SERIAL_NUMBER : int + the number of the `SERIAL_NUMBER` register + """ + WHO_AM_I = 0x00 HW_VERSION_H = 0x01 HW_VERSION_L = 0x02 diff --git a/pyharp/messages.py b/pyharp/messages.py index 2b5aa46..6f651bf 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -18,14 +18,6 @@ class HarpMessage: the message type length : int the length parameter of the Harp message - """ - Calculates the checksum of the Harp message. - - Returns - ------- - int - the value of the checksum - """ address : int the address of the register to which the Harp message refers to port : int @@ -186,14 +178,14 @@ def __init__( + int.from_bytes(frame[9:11], byteorder="little", signed=False) * 32e-6 ) # Timestamp is junk if it's not present. - if not (self.payload_type.value & PayloadType.Timestamp.value): + if not (self.payload_type & PayloadType.Timestamp): self._timestamp = None def _parse_payload(self, raw_payload) -> list[int]: """return the payload as a list of ints after parsing it from the raw payload.""" - is_signed = True if (self.payload_type.value & 0x80) else False - is_float = True if (self.payload_type.value & 0x40) else False - bytes_per_word = self.payload_type.value & 0x07 + is_signed = True if (self.payload_type & 0x80) else False + is_float = True if (self.payload_type & 0x40) else False + bytes_per_word = self.payload_type & 0x07 payload_len = len(raw_payload) # payload length in bytes. word_chunks = [ @@ -219,7 +211,7 @@ def __str__(self): if self.payload_type in [PayloadType.Float, PayloadType.TimestampedFloat]: format_str = ".6f" else: - bytes_per_word = self.payload_type.value & 0x07 + bytes_per_word = self.payload_type & 0x07 format_str = f"0{bytes_per_word}b" payload_str = "".join(f"{item:{format_str}} " for item in self.payload) @@ -262,13 +254,13 @@ class ReadHarpMessage(HarpMessage): def __init__(self, payload_type: PayloadType, address: int): self._frame = bytearray() - self._frame.append(self.MESSAGE_TYPE.value) + self._frame.append(self.MESSAGE_TYPE) length: int = 4 self._frame.append(length) self._frame.append(address) self._frame.append(self.DEFAULT_PORT) - self._frame.append(payload_type.value) + self._frame.append(payload_type) self._frame.append(self.calculate_checksum()) @@ -333,12 +325,12 @@ def __init__( payload += val.to_bytes(byte_size, byteorder="little", signed=signed) # Build the frame - self._frame.append(self.MESSAGE_TYPE.value) + self._frame.append(self.MESSAGE_TYPE) # Length is BASE_LENGTH + payload size self._frame.append(self.BASE_LENGTH + len(payload)) self._frame.append(address) self._frame.append(self.DEFAULT_PORT) - self._frame.append(payload_type.value) + self._frame.append(payload_type) self._frame += payload self._frame.append(self.calculate_checksum()) From c67742931b768b446ff369025af05a4190cf8806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Grilo?= Date: Fri, 4 Apr 2025 18:20:54 +0100 Subject: [PATCH 072/267] Refactor: rename functions to follow a consistent naming pattern --- pyharp/device.py | 48 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/pyharp/device.py b/pyharp/device.py index f572781..4f849e6 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -100,10 +100,10 @@ def load(self) -> None: self.HW_VERSION_H = self._read_hw_version_h() self.HW_VERSION_L = self._read_hw_version_l() self.ASSEMBLY_VERSION = self._read_assembly_version() - self.HARP_VERSION_H = self._read_harp_h_version() - self.HARP_VERSION_L = self._read_harp_l_version() - self.FIRMWARE_VERSION_H = self._read_fw_h_version() - self.FIRMWARE_VERSION_L = self._read_fw_l_version() + self.HARP_VERSION_H = self._read_harp_version_h() + self.HARP_VERSION_L = self._read_harp_version_l() + self.FIRMWARE_VERSION_H = self._read_fw_version_h() + self.FIRMWARE_VERSION_L = self._read_fw_version_l() self.DEVICE_NAME = self._read_device_name() self.SERIAL_NUMBER = self._read_serial_number() @@ -858,28 +858,60 @@ def _read_assembly_version(self) -> int: return reply.payload_as_int() - def _read_harp_h_version(self) -> int: + def _read_harp_version_h(self) -> int: + """ + Reads the value stored in the `HARP_VERSION_H` register. + + Returns + ------- + int + the value of the `HARP_VERSION_H` register. + """ address = CommonRegisters.HARP_VERSION_H reply: ReplyHarpMessage = self.read_u8(address, dump=False) return reply.payload_as_int() - def _read_harp_l_version(self) -> int: + def _read_harp_version_l(self) -> int: + """ + Reads the value stored in the `HARP_VERSION_L` register. + + Returns + ------- + int + the value of the `HARP_VERSION_L` register. + """ address = CommonRegisters.HARP_VERSION_L reply: ReplyHarpMessage = self.read_u8(address, dump=False) return reply.payload_as_int() - def _read_fw_h_version(self) -> int: + def _read_fw_version_h(self) -> int: + """ + Reads the value stored in the `FW_VERSION_H` register. + + Returns + ------- + int + the value of the `FW_VERSION_H` register. + """ address = CommonRegisters.FIRMWARE_VERSION_H reply: ReplyHarpMessage = self.read_u8(address, dump=False) return reply.payload_as_int() - def _read_fw_l_version(self) -> int: + def _read_fw_version_l(self) -> int: + """ + Reads the value stored in the `FW_VERSION_L` register. + + Returns + ------- + int + the value of the `FW_VERSION_L` register. + """ address = CommonRegisters.FIRMWARE_VERSION_L reply: ReplyHarpMessage = self.read_u8(address, dump=False) From 74dd5c4828051233c028d6f565c30254775a0a7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Grilo?= Date: Fri, 4 Apr 2025 18:21:33 +0100 Subject: [PATCH 073/267] Refactor: move DeviceMode to base.py --- pyharp/base.py | 7 +++++++ pyharp/device.py | 10 +--------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/pyharp/base.py b/pyharp/base.py index f367820..388ef5b 100644 --- a/pyharp/base.py +++ b/pyharp/base.py @@ -144,3 +144,10 @@ class CommonRegisters(IntEnum): RESET_DEV = 0x0B DEVICE_NAME = 0x0C SERIAL_NUMBER = 0x0D + + +class DeviceMode(IntEnum): + Standby = 0 + Active = 1 + Reserved = 2 + Speed = 3 diff --git a/pyharp/device.py b/pyharp/device.py index 4f849e6..6205a5a 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -2,25 +2,17 @@ import logging import queue -from enum import Enum from pathlib import Path from typing import List, Optional, Union import serial -from pyharp.base import CommonRegisters, PayloadType +from pyharp.base import CommonRegisters, DeviceMode, PayloadType from pyharp.device_names import device_names from pyharp.harp_serial import HarpSerial from pyharp.messages import ReadHarpMessage, ReplyHarpMessage, WriteHarpMessage -class DeviceMode(Enum): - Standby = 0 - Active = 1 - Reserved = 2 - Speed = 3 - - class Device: """ The `Device` class provides the interface for interacting with Harp devices. This implementation of the Harp device was based on the official documentation available on the [harp-tech website](https://harp-tech.org/protocol/Device.html). From df43bdef69a00cac09daf825de4e9ddbc56d95ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Grilo?= Date: Fri, 4 Apr 2025 18:23:04 +0100 Subject: [PATCH 074/267] Refactor: make some class members private --- pyharp/device.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pyharp/device.py b/pyharp/device.py index 6205a5a..8576559 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -55,7 +55,11 @@ class Device: DEVICE_NAME: str SERIAL_NUMBER: int - TIMEOUT_S = 1.0 + _ser: HarpSerial + _dump_file_path: Path + _read_timeout_s: float + + _TIMEOUT_S: float = 1.0 def __init__( self, @@ -79,7 +83,9 @@ def __init__( self._dump_file_path = None else: self._dump_file_path = Path() / dump_file_path - self.read_timeout_s = read_timeout_s + self._read_timeout_s = read_timeout_s + + # Connect to the Harp device and load the data stored in the device's common registers self.connect() self.load() @@ -120,7 +126,7 @@ def connect(self) -> None: self._ser = HarpSerial( self._serial_port, # "/dev/tty.usbserial-A106C8O9" baudrate=1000000, - timeout=self.TIMEOUT_S, + timeout=self._TIMEOUT_S, parity=serial.PARITY_NONE, stopbits=1, bytesize=8, @@ -297,7 +303,7 @@ def _read(self) -> Union[ReplyHarpMessage, None]: the incoming Harp message in case it exists """ try: - return self._ser.msg_q.get(block=True, timeout=self.read_timeout_s) + return self._ser.msg_q.get(block=True, timeout=self._read_timeout_s) except queue.Empty: return None From 27dc1c864877692676961fb051b85d25e7a0c42b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Grilo?= Date: Fri, 4 Apr 2025 18:25:00 +0100 Subject: [PATCH 075/267] Refactor: modify way of sending write harp messages --- pyharp/device.py | 54 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/pyharp/device.py b/pyharp/device.py index 8576559..0ef3c51 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -164,8 +164,11 @@ def dump_registers(self) -> list: """ address = CommonRegisters.OPERATION_CTRL reg_value = self.read_u8(address).payload_as_int() - reg_value |= 0x08 # Assert DUMP bit - self._ser.write(WriteHarpMessage(PayloadType.U8, address, reg_value).frame) + # Assert DUMP bit + reg_value |= 0x08 + self.write_u8(address, reg_value) + + # Receive the contents of all registers as Harp Read Reply Messages replies = [] while True: msg = self._read() @@ -193,9 +196,14 @@ def set_mode(self, mode: DeviceMode) -> ReplyHarpMessage: # Read register first reg_value = self.read_u8(address).payload_as_int() - reg_value &= ~0x03 # mask off old mode. - reg_value |= mode.value - reply = self.send(WriteHarpMessage(PayloadType.U8, address, reg_value).frame) + + # Clear old operation mode + reg_value &= ~0x03 + + # Set new operation mode + reg_value |= mode + reply = self.write_u8(address, reg_value) + return reply def enable_status_led(self) -> ReplyHarpMessage: @@ -211,8 +219,10 @@ def enable_status_led(self) -> ReplyHarpMessage: # Read register first reg_value = self.read_u8(address).payload_as_int() - reg_value |= (1 << 5) - reply = self.send(WriteHarpMessage(PayloadType.U8, address, reg_value).frame) + reg_value |= 1 << 5 + reply = self.write_u8(address, reg_value) + + return reply def disable_status_led(self) -> ReplyHarpMessage: """ @@ -228,7 +238,9 @@ def disable_status_led(self) -> ReplyHarpMessage: # Read register first reg_value = self.read_u8(address).payload_as_int() reg_value &= ~(1 << 5) - reply = self.send(WriteHarpMessage(PayloadType.U8, address, reg_value).frame) + reply = self.write_u8(address, reg_value) + + return reply def enable_alive_en(self) -> ReplyHarpMessage: """ @@ -243,8 +255,10 @@ def enable_alive_en(self) -> ReplyHarpMessage: # Read register first reg_value = self.read_u8(address).payload_as_int() - reg_value |= (1 << 7) - reply = self.send(WriteHarpMessage(PayloadType.U8, address, reg_value).frame) + reg_value |= 1 << 7 + reply = self.write_u8(address, reg_value) + + return reply def disable_alive_en(self) -> ReplyHarpMessage: """ @@ -259,13 +273,25 @@ def disable_alive_en(self) -> ReplyHarpMessage: # Read register first reg_value = self.read_u8(address).payload[0] - reg_value &= (1 << 7) ^ 0xFF # bitwise ~ operator substitute for Python ints. - reply = self.send(WriteHarpMessage(PayloadType.U8, address, reg_value).frame) + reg_value &= ~(1 << 7) + reply = self.write_u8(address, reg_value) - def reset_device(self): + return reply + + def reset_device(self) -> ReplyHarpMessage: + """ + Resets the device and reboots with all the registers with the default values. Beware that the EEPROM will be erased. More information on the reset device register can be found [here](https://harp-tech.org/protocol/Device.html#r_reset_dev-u8--reset-device-and-save-non-volatile-registers). + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message + """ address = CommonRegisters.RESET_DEV reset_value = 0x01 - self._ser.write(WriteHarpMessage(PayloadType.U8, address, reset_value).frame) + reply = self.write_u8(address, reset_value) + + return reply def send(self, message_bytes: bytearray, dump: bool = True) -> ReplyHarpMessage: """ From 355e90c3cb36469ca96623e99d86a20ece75c816 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Grilo?= Date: Fri, 4 Apr 2025 18:25:29 +0100 Subject: [PATCH 076/267] Refactor: misc changes --- pyharp/device.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pyharp/device.py b/pyharp/device.py index 0ef3c51..b07db19 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -65,7 +65,7 @@ def __init__( self, serial_port: str, dump_file_path: Optional[str] = None, - read_timeout_s=1, + read_timeout_s: float = 1, ): """ Parameters @@ -114,10 +114,12 @@ def info(self) -> None: print(f"* HW version: {self.HW_VERSION_H}.{self.HW_VERSION_L}") print(f"* Assembly version: {self.ASSEMBLY_VERSION}") print(f"* HARP version: {self.HARP_VERSION_H}.{self.HARP_VERSION_L}") - print(f"* Firmware version: {self.FIRMWARE_VERSION_H}.{self.FIRMWARE_VERSION_L}") + print( + f"* Firmware version: {self.FIRMWARE_VERSION_H}.{self.FIRMWARE_VERSION_L}" + ) print(f"* Device user name: {self.DEVICE_NAME}") print(f"* Serial number: {self.SERIAL_NUMBER}") - print(f"* Mode: {self.read_device_mode().name}") + print(f"* Mode: {self._read_device_mode().name}") def connect(self) -> None: """ From 2adbdc39da82596e330788c7df927ba49a1ac621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Grilo?= Date: Sun, 6 Apr 2025 14:05:16 +0100 Subject: [PATCH 077/267] Feature: add HarpMessage.create static method --- pyharp/messages.py | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/pyharp/messages.py b/pyharp/messages.py index 6f651bf..b3c905a 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -149,8 +149,41 @@ def parse(frame: bytearray) -> ReplyHarpMessage: """ return ReplyHarpMessage(frame) + @staticmethod + def create( + message_type: MessageType, + address: int, + payload_type: PayloadType, + value: int | list[int] | float | list[float] = None, + ) -> HarpMessage: + """ + Creates a Harp message. + + Parameters + ---------- + message_type : MessageType + the message type. It can only be of type READ or WRITE + address : int + the address of the register that the message will interact with + payload_type : PayloadType + the payload type + value: int | list[int] | float | list[float], optional + the payload of the message. If message_type == MessageType.WRITE, the value cannot be None + """ + if message_type == MessageType.READ: + return ReadHarpMessage(payload_type, address) + elif message_type == MessageType.WRITE and value is not None: + return WriteHarpMessage(payload_type, address, value) + elif message_type != MessageType.READ and message_type != MessageType.WRITE: + raise Exception( + "The only valid message types are MessageType.READ and MessageType.Write!" + ) + else: + raise Exception( + "The value cannot be None is message type is equal to MessageType.WRITE!" + ) + -# A Response Message from a harp device. class ReplyHarpMessage(HarpMessage): """ A Response Message from a harp device. From 152a28de38eca40314fef33db14184d0fd0c52e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Grilo?= Date: Sun, 6 Apr 2025 14:10:30 +0100 Subject: [PATCH 078/267] Docs: add comments to the remaining code --- pyharp/base.py | 2 + pyharp/device.py | 2 +- pyharp/harp_serial.py | 80 ++++++++++++++++++++++--- pyharp/messages.py | 136 ++++++++++++++++++++++++++++++++++++------ 4 files changed, 194 insertions(+), 26 deletions(-) diff --git a/pyharp/base.py b/pyharp/base.py index 388ef5b..6962a0e 100644 --- a/pyharp/base.py +++ b/pyharp/base.py @@ -100,6 +100,8 @@ class CommonRegisters(IntEnum): """ An enumeration with the registers that are common to every Harp device. More information on the common registers can be found [here](https://harp-tech.org/protocol/Device.html#table---list-of-available-common-registers). + Attributes + ---------- WHO_AM_I : int the number of the `WHO_AM_I` register HW_VERSION_H : int diff --git a/pyharp/device.py b/pyharp/device.py index b07db19..4e4f8d2 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -71,7 +71,7 @@ def __init__( Parameters ---------- serial_port : str - the serial port used to establish the connection with the Harp device. It must be denoted as `ttyUSBx` in Linux and `COMx` in Windows, where `x` is the number of the serial port + the serial port used to establish the connection with the Harp device. It must be denoted as `/dev/ttyUSBx` in Linux and `COMx` in Windows, where `x` is the number of the serial port dump_file_path: str, optional the binary file to which all Harp messages will be written read_timeout_s: float, optional diff --git a/pyharp/harp_serial.py b/pyharp/harp_serial.py index 3b9d4a2..bed20b3 100644 --- a/pyharp/harp_serial.py +++ b/pyharp/harp_serial.py @@ -1,8 +1,9 @@ -from typing import Union -from functools import partial import logging import queue import threading +from functools import partial +from typing import Union + import serial import serial.threaded @@ -10,40 +11,91 @@ class HarpSerialProtocol(serial.threaded.Protocol): + """ + The `HarpSerialProtocol` class deals with the data received from the serial communication. + """ + _read_q: queue.Queue - def __init__(self, _read_q: queue.Queue, *args, **kwargs): - self._read_q = _read_q + def __init__(self, read_q: queue.Queue, *args, **kwargs): + """ + Parameters + ---------- + read_q : queue.Queue + the queue to where the data received will be put + """ + self._read_q = read_q super().__init__(*args, **kwargs) def connection_made(self, transport: serial.threaded.ReaderThread) -> None: - # print(f"Connected to {transport.serial.port}") + """ + _TODO_ + + Parameters + ---------- + transport : serial.threaded.ReaderThread + _TODO_ + """ return super().connection_made(transport) def data_received(self, data: bytes) -> None: + """ + Receives data from the serial commmunication. + + Parameters + ---------- + data : bytes + the data received from the serial communication + """ for byte in data: self._read_q.put(byte) return super().data_received(data) def connection_lost(self, exc: Union[BaseException, None]) -> None: - # print(f"Lost connection!") + """ + _TODO_ + + Parameters + ---------- + exc : exc: Union[BaseException, None] + _TODO_ + """ return super().connection_lost(exc) class HarpSerial: + """ + The `HarpSerial` deals with the received Harp messages and separates the events from the remaining messages. + + Attributes + ---------- + msg_q : queue.Queue + the queue containing the Harp messages that are not of the type `MessageType.EVENT` + event_q : queue.Queue + the queue containing the Harp messages of `MessageType.EVENT` + """ msg_q: queue.Queue event_q: queue.Queue def __init__(self, serial_port: str, **kwargs): + """ + Parameters + ---------- + serial_port : str + the serial port used to establish the connection with the Harp device. It must be denoted as `/dev/ttyUSBx` in Linux and `COMx` in Windows, where `x` is the number of the serial port + """ + # Connect to the Harp device self._ser = serial.Serial(serial_port, **kwargs) self.log = logging.getLogger(f"{__name__}.{self.__class__.__name__}") + # Initialize the message queues self._read_q = queue.Queue() self.msg_q = queue.Queue() self.event_q = queue.Queue() + # Start the thread with the `HarpSerialProtocol` self._reader = serial.threaded.ReaderThread( self._ser, partial(HarpSerialProtocol, self._read_q), @@ -51,6 +103,7 @@ def __init__(self, serial_port: str, **kwargs): self._reader.start() transport, protocol = self._reader.connect() + # Start the thread that parses and separates the events from the remaining messages self._parse_thread = threading.Thread( target=self.parse_harp_msgs_threaded, daemon=True, @@ -58,26 +111,39 @@ def __init__(self, serial_port: str, **kwargs): self._parse_thread.start() def close(self): + """ + Closes the serial port. + """ self._reader.close() def write(self, data): + """ + Writes data to the Harp device. + """ self._reader.write(data) def parse_harp_msgs_threaded(self): + """ + Parses the Harp messages and separates the events from the remaining messages. + """ while True: - message_type = self._read_q.get(1) # byte array with only one byte + # Gets the Harp message bytes based on the length byte of the message + message_type = self._read_q.get(1) message_length = self._read_q.get(1) message_content = bytes([self._read_q.get() for _ in range(message_length)]) self.log.debug(f"reply (type): {message_type}") self.log.debug(f"reply (length): {message_length}") self.log.debug(f"reply (payload): {message_content}") + # Reconstructs the message into a bytearray frame = bytearray() frame.append(message_type) frame.append(message_length) frame += message_content + # Parses the bytearray into a ReplyHarpMessage object msg = HarpMessage.parse(frame) + # Puts the parsed Harp message into the correct queue if msg.message_type == MessageType.EVENT: self.event_q.put(msg) else: diff --git a/pyharp/messages.py b/pyharp/messages.py index b3c905a..690e987 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -186,7 +186,14 @@ def create( class ReplyHarpMessage(HarpMessage): """ - A Response Message from a harp device. + A response message from a Harp device. + + Attributes + ---------- + payload : Union[int, list[int]] + the message payload formatted as the appropriate type + timestamp : float + the Harp timestamp at which the message was sent """ def __init__( @@ -194,32 +201,48 @@ def __init__( frame: bytearray, ): """ - - :param frame: the serialized message frame. + Parameters + ---------- + frame : bytearray + the Harp message in bytearray format """ self._frame = frame - # retrieve all content from 11 (where payload starts) until the checksum (not inclusive) + # Retrieve all content from 11 (where payload starts) until the checksum (not inclusive) self._raw_payload = frame[11:-1] - self._payload = self._parse_payload( - self._raw_payload - ) # payload formatted as list[payload type] + + # Format payload as list[PayloadType] + self._payload = self._parse_payload(self._raw_payload) # Assign timestamp after _payload since @properties all rely on self._payload. self._timestamp = ( int.from_bytes(frame[5:9], byteorder="little", signed=False) + int.from_bytes(frame[9:11], byteorder="little", signed=False) * 32e-6 ) + # Timestamp is junk if it's not present. if not (self.payload_type & PayloadType.Timestamp): self._timestamp = None - def _parse_payload(self, raw_payload) -> list[int]: - """return the payload as a list of ints after parsing it from the raw payload.""" + # TODO: handle the case of the payload being of type float + def _parse_payload(self, raw_payload: bytearray) -> list[int]: + """ + Returns the payload as a list of ints after parsing it from the raw payload. + + Parameters + ---------- + raw_payload : bytearray + the bytearray containing the raw payload + + Returns + ------- + list[int] + the list of ints containing the payload + """ is_signed = True if (self.payload_type & 0x80) else False is_float = True if (self.payload_type & 0x40) else False bytes_per_word = self.payload_type & 0x07 - payload_len = len(raw_payload) # payload length in bytes. + payload_len = len(raw_payload) word_chunks = [ raw_payload[i : i + bytes_per_word] @@ -230,15 +253,29 @@ def _parse_payload(self, raw_payload) -> list[int]: int.from_bytes(chunk, byteorder="little", signed=is_signed) for chunk in word_chunks ] - else: # handle float case. + else: # TODO: handle float case return [struct.unpack(" str: + """ + Prints debug representation of the reply message. + + Returns + ------- + str + the debug representation of the reply message + """ return self.__str__() + f"\r\nRaw Frame: {self.frame}" - def __str__(self): - """Print friendly representation of a reply message.""" + def __str__(self) -> str: + """ + Prints friendly representation of the reply message. + + Returns + ------- + str + the representation of the reply message + """ payload_str = "" format_str = "" if self.payload_type in [PayloadType.Float, PayloadType.TimestampedFloat]: @@ -261,27 +298,72 @@ def __str__(self): + f"Checksum: {self.checksum}" ) + # TODO: handle float case @property def payload(self) -> Union[int, list[int]]: - """return the payload formatted as the appropriate type.""" + """ + The message payload formatted as the appropriate type. + + Returns + ------- + Union[int, list[int]] + the message payload formatted as the appropriate type + """ return self._payload @property def timestamp(self) -> float: + """ + The Harp timestamp at which the message was sent. + + Returns + ------- + float + the Harp timestamp at which the message was sent + """ return self._timestamp + # TODO: does this function makes sense since self.payload() already exists? def payload_as_int(self) -> int: + """ + Returns the payload as an int. + + Returns + ------- + int + the payload parsed as an int + """ return self.payload[0] def payload_as_string(self) -> str: + """ + Returns the payload as a str. + + Returns + ------- + str + the payload parsed as a str + """ return self._raw_payload.decode("utf-8") + # TODO: handle float case and/or delete functional altogether def payload_as_float(self) -> float: - return self.payload[0] # already parsed. + """ + Returns the payload as a float. + + Returns + ------- + float + the payload parsed as a float + """ + return self.payload[0] -# A Read Request Message sent to a harp device. class ReadHarpMessage(HarpMessage): + """ + A read Harp message sent to a Harp device. + """ + MESSAGE_TYPE: int = MessageType.READ def __init__(self, payload_type: PayloadType, address: int): @@ -298,6 +380,15 @@ def __init__(self, payload_type: PayloadType, address: int): class WriteHarpMessage(HarpMessage): + """ + A write Harp message sent to a Harp device. + + Attributes + ---------- + payload : Union[int, list[int]] + the payload sent in the write Harp message + """ + BASE_LENGTH: int = 5 BASE_LENGTH: int = 4 MESSAGE_TYPE: int = MessageType.WRITE @@ -367,8 +458,17 @@ def __init__( self._frame += payload self._frame.append(self.calculate_checksum()) + # TODO: handle float and array cases @property def payload(self) -> Union[int, list[int]]: + """ + The payload sent in the write Harp message. + + Returns + ------- + Union[int, list[int]] + the payload sent in the write Harp message + """ match self.payload_type: case PayloadType.U8: return self._frame[5] From 09e0dd6a948e1801ac6f356b2c148dfa35ac6734 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Grilo?= Date: Sun, 6 Apr 2025 14:13:31 +0100 Subject: [PATCH 079/267] Feature: add classes in base.py to root pyharp namespace --- pyharp/__init__.py | 2 ++ pyharp/device.py | 2 +- pyharp/messages.py | 2 +- tests/test_device.py | 1 + tests/test_messages.py | 2 +- 5 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pyharp/__init__.py b/pyharp/__init__.py index 3dc1f76..fc569a5 100644 --- a/pyharp/__init__.py +++ b/pyharp/__init__.py @@ -1 +1,3 @@ +from pyharp.base import CommonRegisters, DeviceMode, MessageType, PayloadType + __version__ = "0.1.0" diff --git a/pyharp/device.py b/pyharp/device.py index 4e4f8d2..b7fa03d 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -7,7 +7,7 @@ import serial -from pyharp.base import CommonRegisters, DeviceMode, PayloadType +from pyharp import CommonRegisters, DeviceMode, MessageType, PayloadType from pyharp.device_names import device_names from pyharp.harp_serial import HarpSerial from pyharp.messages import ReadHarpMessage, ReplyHarpMessage, WriteHarpMessage diff --git a/pyharp/messages.py b/pyharp/messages.py index 690e987..526948d 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -3,7 +3,7 @@ import struct from typing import List, Union -from pyharp.base import MessageType, PayloadType +from pyharp import MessageType, PayloadType class HarpMessage: diff --git a/tests/test_device.py b/tests/test_device.py index 00d9d3b..798866e 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -1,5 +1,6 @@ import time +from pyharp import MessageType, PayloadType from pyharp.device import Device from pyharp.messages import HarpMessage, ReplyHarpMessage diff --git a/tests/test_messages.py b/tests/test_messages.py index 93223b4..2be8ce0 100644 --- a/tests/test_messages.py +++ b/tests/test_messages.py @@ -1,4 +1,4 @@ -from pyharp.base import CommonRegisters, PayloadType +from pyharp import CommonRegisters, PayloadType from pyharp.messages import MessageType, ReadHarpMessage, WriteHarpMessage DEFAULT_ADDRESS = 42 From c7fea61b3a7d97a231cee86d7efecae994a5c518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Grilo?= Date: Sun, 6 Apr 2025 14:20:39 +0100 Subject: [PATCH 080/267] Feature: remove read_xx and write_xx Device methods --- README.md | 12 +- docs/index.md | 12 +- pyharp/device.py | 565 ++++++++----------------------------------- tests/test_device.py | 10 +- 4 files changed, 122 insertions(+), 477 deletions(-) diff --git a/README.md b/README.md index 335330f..d1485ef 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,9 @@ pip install pyharp ## Quick Start ```python +from pyharp import MessageType, PayloadType from pyharp.device import Device +from pyharp.messages import HarpMessage # Connect to a device device = Device("/dev/ttyUSB0") @@ -25,10 +27,10 @@ device.info() register_address = 32 # Read from register -value = device.read_u8(register_address) +value = device.send(HarpMessage.create(MessageType.READ, register_address, PayloadType.U8).frame) # Write to register -device.write_u8(register_address, value) +device.send(HarpMessage.create(MessageType.WRITE, register_address, PayloadType.U8, value).frame) # Disconnect when done device.disconnect() @@ -37,7 +39,9 @@ device.disconnect() or using the `with` statement: ```python +from pyharp import MessageType, PayloadType from pyharp.device import Device +from pyharp.messages import HarpMessage with Device("/dev/ttyUSB0") as device: # Get device information @@ -47,10 +51,10 @@ with Device("/dev/ttyUSB0") as device: register_address = 32 # Read from register - value = device.read_u8(register_address) + value = device.send(HarpMessage.create(MessageType.READ, register_address, PayloadType.U8).frame) # Write to register - device.write_u8(register_address, value) + device.send(HarpMessage.create(MessageType.WRITE, register_address, PayloadType.U8, value).frame) ``` ## for Linux diff --git a/docs/index.md b/docs/index.md index 335330f..d1485ef 100644 --- a/docs/index.md +++ b/docs/index.md @@ -13,7 +13,9 @@ pip install pyharp ## Quick Start ```python +from pyharp import MessageType, PayloadType from pyharp.device import Device +from pyharp.messages import HarpMessage # Connect to a device device = Device("/dev/ttyUSB0") @@ -25,10 +27,10 @@ device.info() register_address = 32 # Read from register -value = device.read_u8(register_address) +value = device.send(HarpMessage.create(MessageType.READ, register_address, PayloadType.U8).frame) # Write to register -device.write_u8(register_address, value) +device.send(HarpMessage.create(MessageType.WRITE, register_address, PayloadType.U8, value).frame) # Disconnect when done device.disconnect() @@ -37,7 +39,9 @@ device.disconnect() or using the `with` statement: ```python +from pyharp import MessageType, PayloadType from pyharp.device import Device +from pyharp.messages import HarpMessage with Device("/dev/ttyUSB0") as device: # Get device information @@ -47,10 +51,10 @@ with Device("/dev/ttyUSB0") as device: register_address = 32 # Read from register - value = device.read_u8(register_address) + value = device.send(HarpMessage.create(MessageType.READ, register_address, PayloadType.U8).frame) # Write to register - device.write_u8(register_address, value) + device.send(HarpMessage.create(MessageType.WRITE, register_address, PayloadType.U8, value).frame) ``` ## for Linux diff --git a/pyharp/device.py b/pyharp/device.py index b7fa03d..27212fd 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -3,14 +3,14 @@ import logging import queue from pathlib import Path -from typing import List, Optional, Union +from typing import Optional, Union import serial from pyharp import CommonRegisters, DeviceMode, MessageType, PayloadType from pyharp.device_names import device_names from pyharp.harp_serial import HarpSerial -from pyharp.messages import ReadHarpMessage, ReplyHarpMessage, WriteHarpMessage +from pyharp.messages import HarpMessage, ReplyHarpMessage class Device: @@ -151,7 +151,9 @@ def _read_device_mode(self) -> DeviceMode: the current device mode """ address = CommonRegisters.OPERATION_CTRL - reply = self.read_u8(address) + reply = self.send( + HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame + ) return DeviceMode(reply.payload_as_int() & 0x03) def dump_registers(self) -> list: @@ -165,10 +167,16 @@ def dump_registers(self) -> list: the list containing the reply Harp messages for all the device's registers """ address = CommonRegisters.OPERATION_CTRL - reg_value = self.read_u8(address).payload_as_int() + reg_value = self.send( + HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame + ).payload_as_int() # Assert DUMP bit reg_value |= 0x08 - self.write_u8(address, reg_value) + self.send( + HarpMessage.create( + MessageType.WRITE, address, PayloadType.U8, reg_value + ).frame + ) # Receive the contents of all registers as Harp Read Reply Messages replies = [] @@ -197,14 +205,20 @@ def set_mode(self, mode: DeviceMode) -> ReplyHarpMessage: address = CommonRegisters.OPERATION_CTRL # Read register first - reg_value = self.read_u8(address).payload_as_int() + reg_value = self.send( + HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame + ).payload_as_int() # Clear old operation mode reg_value &= ~0x03 # Set new operation mode reg_value |= mode - reply = self.write_u8(address, reg_value) + reply = self.send( + HarpMessage.create( + MessageType.WRITE, address, PayloadType.U8, reg_value + ).frame + ) return reply @@ -220,9 +234,15 @@ def enable_status_led(self) -> ReplyHarpMessage: address = CommonRegisters.OPERATION_CTRL # Read register first - reg_value = self.read_u8(address).payload_as_int() + reg_value = self.send( + HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame + ).payload_as_int() reg_value |= 1 << 5 - reply = self.write_u8(address, reg_value) + reply = self.send( + HarpMessage.create( + MessageType.WRITE, address, PayloadType.U8, reg_value + ).frame + ) return reply @@ -238,9 +258,15 @@ def disable_status_led(self) -> ReplyHarpMessage: address = CommonRegisters.OPERATION_CTRL # Read register first - reg_value = self.read_u8(address).payload_as_int() + reg_value = self.send( + HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame + ).payload_as_int() reg_value &= ~(1 << 5) - reply = self.write_u8(address, reg_value) + reply = self.send( + HarpMessage.create( + MessageType.WRITE, address, PayloadType.U8, reg_value + ).frame + ) return reply @@ -256,9 +282,15 @@ def enable_alive_en(self) -> ReplyHarpMessage: address = CommonRegisters.OPERATION_CTRL # Read register first - reg_value = self.read_u8(address).payload_as_int() + reg_value = self.send( + HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame + ).payload_as_int() reg_value |= 1 << 7 - reply = self.write_u8(address, reg_value) + reply = self.send( + HarpMessage.create( + MessageType.WRITE, address, PayloadType.U8, reg_value + ).frame + ) return reply @@ -274,9 +306,15 @@ def disable_alive_en(self) -> ReplyHarpMessage: address = CommonRegisters.OPERATION_CTRL # Read register first - reg_value = self.read_u8(address).payload[0] + reg_value = self.send( + HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame + ).payload[0] reg_value &= ~(1 << 7) - reply = self.write_u8(address, reg_value) + reply = self.send( + HarpMessage.create( + MessageType.WRITE, address, PayloadType.U8, reg_value + ).frame + ) return reply @@ -291,7 +329,11 @@ def reset_device(self) -> ReplyHarpMessage: """ address = CommonRegisters.RESET_DEV reset_value = 0x01 - reply = self.write_u8(address, reset_value) + reply = self.send( + HarpMessage.create( + MessageType.WRITE, address, PayloadType.U8, reset_value + ).frame + ) return reply @@ -372,447 +414,6 @@ def event_count(self) -> int: """ return self._ser.event_q.qsize() - def read_u8(self, address: int, dump: bool = True) -> ReplyHarpMessage: - """ - Reads the value of a register of type U8. - - Parameters - ---------- - address : int - the register to be read - dump : bool, optional - indicates whether the reply message should be dumped or not - - Returns - ------- - ReplyHarpMessage - the reply to the Harp message that will contain the value read from the register - """ - return self.send( - ReadHarpMessage(payload_type=PayloadType.U8, address=address).frame, dump - ) - - def read_s8(self, address: int, dump: bool = True) -> ReplyHarpMessage: - """ - Reads the value of a register of type S8. - - Parameters - ---------- - address : int - the register to be read - dump : bool, optional - indicates whether the reply message should be dumped or not - - Returns - ------- - ReplyHarpMessage - the reply to the Harp message that will contain the value read from the register - """ - return self.send( - ReadHarpMessage(payload_type=PayloadType.S8, address=address).frame, dump - ) - - def read_u16(self, address: int, dump: bool = True) -> ReplyHarpMessage: - """ - Reads the value of a register of type U16. - - Parameters - ---------- - address : int - the register to be read - dump : bool, optional - indicates whether the reply message should be dumped or not - - Returns - ------- - ReplyHarpMessage - the reply to the Harp message that will contain the value read from the register - """ - return self.send( - ReadHarpMessage(payload_type=PayloadType.U16, address=address).frame, dump - ) - - def read_s16(self, address: int, dump: bool = True) -> ReplyHarpMessage: - """ - Reads the value of a register of type S16. - - Parameters - ---------- - address : int - the register to be read - dump : bool, optional - indicates whether the reply message should be dumped or not - - Returns - ------- - ReplyHarpMessage - the reply to the Harp message that will contain the value read from the register - """ - return self.send( - ReadHarpMessage(payload_type=PayloadType.S16, address=address).frame, dump - ) - - def read_u32(self, address: int, dump: bool = True) -> ReplyHarpMessage: - """ - Reads the value of a register of type U32. - - Parameters - ---------- - address : int - the register to be read - dump : bool, optional - indicates whether the reply message should be dumped or not - - Returns - ------- - ReplyHarpMessage - the reply to the Harp message that will contain the value read from the register - """ - return self.send( - ReadHarpMessage(payload_type=PayloadType.U32, address=address).frame, dump - ) - - def read_s32(self, address: int, dump: bool = True) -> ReplyHarpMessage: - """ - Reads the value of a register of type S32. - - Parameters - ---------- - address : int - the register to be read - dump : bool, optional - indicates whether the reply message should be dumped or not - - Returns - ------- - ReplyHarpMessage - the reply to the Harp message that will contain the value read from the register - """ - return self.send( - ReadHarpMessage(payload_type=PayloadType.S32, address=address).frame, dump - ) - - def read_u64(self, address: int, dump: bool = True) -> ReplyHarpMessage: - """ - Reads the value of a register of type U64. - - Parameters - ---------- - address : int - the register to be read - dump : bool, optional - indicates whether the reply message should be dumped or not - - Returns - ------- - ReplyHarpMessage - the reply to the Harp message that will contain the value read from the register - """ - return self.send( - ReadHarpMessage(payload_type=PayloadType.U64, address=address).frame, dump - ) - - def read_s64(self, address: int, dump: bool = True) -> ReplyHarpMessage: - """ - Reads the value of a register of type S64. - - Parameters - ---------- - address : int - the register to be read - dump : bool, optional - indicates whether the reply message should be dumped or not - - Returns - ------- - ReplyHarpMessage - the reply to the Harp message that will contain the value read from the register - """ - return self.send( - ReadHarpMessage(payload_type=PayloadType.S64, address=address).frame, dump - ) - - def read_float(self, address: int, dump: bool = True) -> ReplyHarpMessage: - """ - Reads the value of a register of type Float. - - Parameters - ---------- - address : int - the register to be read - dump : bool, optional - indicates whether the reply message should be dumped or not - - Returns - ------- - ReplyHarpMessage - the reply to the Harp message that will contain the value read from the register - """ - return self.send( - ReadHarpMessage(payload_type=PayloadType.Float, address=address).frame, dump - ) - - def write_u8( - self, address: int, value: int | List[int], dump: bool = True - ) -> ReplyHarpMessage: - """ - Writes the value of a register of type U8. - - Parameters - ---------- - address : int - the register to be written on - value: int | List[int] - the value to be written to the register - dump : bool, optional - indicates whether the reply message should be dumped or not - - Returns - ------- - ReplyHarpMessage - the reply to the Harp message - """ - return self.send( - WriteHarpMessage( - payload_type=PayloadType.U8, - address=address, - value=value, - ).frame, - dump=dump, - ) - - def write_s8( - self, address: int, value: int | List[int], dump: bool = True - ) -> ReplyHarpMessage: - """ - Writes the value of a register of type S8. - - Parameters - ---------- - address : int - the register to be written on - value: int | List[int] - the value to be written to the register - dump : bool, optional - indicates whether the reply message should be dumped or not - - Returns - ------- - ReplyHarpMessage - the reply to the Harp message - """ - return self.send( - WriteHarpMessage( - payload_type=PayloadType.S8, - address=address, - value=value, - ).frame, - dump=dump, - ) - - def write_u16( - self, address: int, value: int | List[int], dump: bool = True - ) -> ReplyHarpMessage: - """ - Writes the value of a register of type U16. - - Parameters - ---------- - address : int - the register to be written on - value: int | List[int] - the value to be written to the register - dump : bool, optional - indicates whether the reply message should be dumped or not - - Returns - ------- - ReplyHarpMessage - the reply to the Harp message - """ - return self.send( - WriteHarpMessage( - payload_type=PayloadType.U16, - address=address, - value=value, - ).frame, - dump=dump, - ) - - def write_s16( - self, address: int, value: int | List[int], dump: bool = True - ) -> ReplyHarpMessage: - """ - Writes the value of a register of type S16. - - Parameters - ---------- - address : int - the register to be written on - value: int | List[int] - the value to be written to the register - dump : bool, optional - indicates whether the reply message should be dumped or not - - Returns - ------- - ReplyHarpMessage - the reply to the Harp message - """ - return self.send( - WriteHarpMessage( - payload_type=PayloadType.S16, - address=address, - value=value, - ).frame, - dump=dump, - ) - - def write_u32( - self, address: int, value: int | List[int], dump: bool = True - ) -> ReplyHarpMessage: - """ - Writes the value of a register of type U32. - - Parameters - ---------- - address : int - the register to be written on - value: int | List[int] - the value to be written to the register - dump : bool, optional - indicates whether the reply message should be dumped or not - - Returns - ------- - ReplyHarpMessage - the reply to the Harp message - """ - return self.send( - WriteHarpMessage( - payload_type=PayloadType.U32, - address=address, - value=value, - ).frame, - dump=dump, - ) - - def write_s32( - self, address: int, value: int | List[int], dump: bool = True - ) -> ReplyHarpMessage: - """ - Writes the value of a register of type S32. - - Parameters - ---------- - address : int - the register to be written on - value: int | List[int] - the value to be written to the register - dump : bool, optional - indicates whether the reply message should be dumped or not - - Returns - ------- - ReplyHarpMessage - the reply to the Harp message - """ - return self.send( - WriteHarpMessage( - payload_type=PayloadType.S32, - address=address, - value=value, - ).frame, - dump=dump, - ) - - def write_u64( - self, address: int, value: int | List[int], dump: bool = True - ) -> ReplyHarpMessage: - """ - Writes the value of a register of type U64. - - Parameters - ---------- - address : int - the register to be written on - value: int | List[int] - the value to be written to the register - dump : bool, optional - indicates whether the reply message should be dumped or not - - Returns - ------- - ReplyHarpMessage - the reply to the Harp message - """ - return self.send( - WriteHarpMessage( - payload_type=PayloadType.U64, - address=address, - value=value, - ).frame, - dump=dump, - ) - - def write_s64( - self, address: int, value: int | List[int], dump: bool = True - ) -> ReplyHarpMessage: - """ - Writes the value of a register of type S64. - - Parameters - ---------- - address : int - the register to be written on - value: int | List[int] - the value to be written to the register - dump : bool, optional - indicates whether the reply message should be dumped or not - - Returns - ------- - ReplyHarpMessage - the reply to the Harp message - """ - return self.send( - WriteHarpMessage( - payload_type=PayloadType.S64, - address=address, - value=value, - ).frame, - dump=dump, - ) - - def write_float( - self, address: int, value: float | List[float], dump: bool = True - ) -> ReplyHarpMessage: - """ - Writes the value of a register of type Float. - - Parameters - ---------- - address : int - the register to be written on - value: int | List[int] - the value to be written to the register - dump : bool, optional - indicates whether the reply message should be dumped or not - - Returns - ------- - ReplyHarpMessage - the reply to the Harp message - """ - return self.send( - WriteHarpMessage( - payload_type=PayloadType.Float, - address=address, - value=value, - ).frame, - dump=dump, - ) - def _read_who_am_i(self) -> int: """ Reads the value stored in the `WHO_AM_I` register. @@ -824,7 +425,10 @@ def _read_who_am_i(self) -> int: """ address = CommonRegisters.WHO_AM_I - reply: ReplyHarpMessage = self.read_u16(address, dump=False) + reply: ReplyHarpMessage = self.send( + HarpMessage.create(MessageType.READ, address, PayloadType.U16).frame, + dump=False, + ) return reply.payload_as_int() @@ -850,7 +454,10 @@ def _read_hw_version_h(self) -> int: """ address = CommonRegisters.HW_VERSION_H - reply: ReplyHarpMessage = self.read_u8(address, dump=False) + reply: ReplyHarpMessage = self.send( + HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame, + dump=False, + ) return reply.payload_as_int() @@ -865,7 +472,10 @@ def _read_hw_version_l(self) -> int: """ address = CommonRegisters.HW_VERSION_L - reply: ReplyHarpMessage = self.read_u8(address, dump=False) + reply: ReplyHarpMessage = self.send( + HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame, + dump=False, + ) return reply.payload_as_int() @@ -880,7 +490,10 @@ def _read_assembly_version(self) -> int: """ address = CommonRegisters.ASSEMBLY_VERSION - reply: ReplyHarpMessage = self.read_u8(address, dump=False) + reply: ReplyHarpMessage = self.send( + HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame, + dump=False, + ) return reply.payload_as_int() @@ -895,7 +508,10 @@ def _read_harp_version_h(self) -> int: """ address = CommonRegisters.HARP_VERSION_H - reply: ReplyHarpMessage = self.read_u8(address, dump=False) + reply: ReplyHarpMessage = self.send( + HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame, + dump=False, + ) return reply.payload_as_int() @@ -910,7 +526,10 @@ def _read_harp_version_l(self) -> int: """ address = CommonRegisters.HARP_VERSION_L - reply: ReplyHarpMessage = self.read_u8(address, dump=False) + reply: ReplyHarpMessage = self.send( + HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame, + dump=False, + ) return reply.payload_as_int() @@ -925,7 +544,10 @@ def _read_fw_version_h(self) -> int: """ address = CommonRegisters.FIRMWARE_VERSION_H - reply: ReplyHarpMessage = self.read_u8(address, dump=False) + reply: ReplyHarpMessage = self.send( + HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame, + dump=False, + ) return reply.payload_as_int() @@ -940,7 +562,10 @@ def _read_fw_version_l(self) -> int: """ address = CommonRegisters.FIRMWARE_VERSION_L - reply: ReplyHarpMessage = self.read_u8(address, dump=False) + reply: ReplyHarpMessage = self.send( + HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame, + dump=False, + ) return reply.payload_as_int() @@ -955,7 +580,10 @@ def _read_device_name(self) -> str: """ address = CommonRegisters.DEVICE_NAME - reply: ReplyHarpMessage = self.read_u8(address, dump=False) + reply: ReplyHarpMessage = self.send( + HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame, + dump=False, + ) return reply.payload_as_string() @@ -970,7 +598,10 @@ def _read_serial_number(self) -> int: """ address = CommonRegisters.SERIAL_NUMBER - reply: ReplyHarpMessage = self.read_u8(address, dump=False) + reply: ReplyHarpMessage = self.send( + HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame, + dump=False, + ) if reply.has_error(): return 0 diff --git a/tests/test_device.py b/tests/test_device.py index 798866e..3a79b1b 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -24,7 +24,9 @@ def test_read_U8() -> None: register: int = 38 read_size: int = 35 # TODO: automatically calculate this! - reply: ReplyHarpMessage = device.read_u8(register) + reply: ReplyHarpMessage = device.send( + HarpMessage.create(MessageType.READ, register, PayloadType.U8).frame + ) assert reply is not None # assert reply.payload_as_int() == write_value @@ -45,7 +47,11 @@ def test_U8() -> None: # assert reply[11] == 0 # what is the default register value?! # write 65 on register 38 - reply: ReplyHarpMessage = device.write_u8(register, write_value) + reply: ReplyHarpMessage = device.send( + HarpMessage.create( + MessageType.WRITE, register, PayloadType.U8, write_value + ).frame + ) assert reply is not None # read register 38 From 8f52e90d6aadf9b7c678c275d58f7c3e85801af5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Grilo?= Date: Sun, 6 Apr 2025 14:25:47 +0100 Subject: [PATCH 081/267] Docs: reorganize examples section --- docs/examples/check_device_id.md | 9 --- docs/examples/code/check_device_id.py | 34 ----------- docs/examples/code/get_info.py | 42 ------------- docs/examples/code/wait_for_events.py | 26 -------- .../code/write_and_read_from_registers.py | 59 ------------------- docs/examples/get_info.md | 9 --- docs/examples/get_info/get_info.md | 12 ++++ docs/examples/get_info/get_info.py | 20 +++++++ docs/examples/index.md | 4 +- .../read_and_write_from_registers.md | 17 ++++++ .../read_and_write_from_registers.py | 33 +++++++++++ docs/examples/wait_for_events.md | 9 --- .../wait_for_events/wait_for_events.md | 12 ++++ .../wait_for_events/wait_for_events.py | 19 ++++++ .../examples/write_and_read_from_registers.md | 9 --- mkdocs.yml | 8 +-- 16 files changed, 119 insertions(+), 203 deletions(-) delete mode 100644 docs/examples/check_device_id.md delete mode 100755 docs/examples/code/check_device_id.py delete mode 100755 docs/examples/code/get_info.py delete mode 100755 docs/examples/code/wait_for_events.py delete mode 100755 docs/examples/code/write_and_read_from_registers.py delete mode 100644 docs/examples/get_info.md create mode 100644 docs/examples/get_info/get_info.md create mode 100755 docs/examples/get_info/get_info.py create mode 100644 docs/examples/read_and_write_from_registers/read_and_write_from_registers.md create mode 100755 docs/examples/read_and_write_from_registers/read_and_write_from_registers.py delete mode 100644 docs/examples/wait_for_events.md create mode 100644 docs/examples/wait_for_events/wait_for_events.md create mode 100755 docs/examples/wait_for_events/wait_for_events.py delete mode 100644 docs/examples/write_and_read_from_registers.md diff --git a/docs/examples/check_device_id.md b/docs/examples/check_device_id.md deleted file mode 100644 index c9946ec..0000000 --- a/docs/examples/check_device_id.md +++ /dev/null @@ -1,9 +0,0 @@ -# Check Device Id - -This example demonstrates connecting to a Harp device and checking its ID: - - -```python -[](./code/check_device_id.py) -``` - \ No newline at end of file diff --git a/docs/examples/code/check_device_id.py b/docs/examples/code/check_device_id.py deleted file mode 100755 index 04297a6..0000000 --- a/docs/examples/code/check_device_id.py +++ /dev/null @@ -1,34 +0,0 @@ -import os - -from pyharp.device import Device -from pyharp.device_names import device_names - -# ON THIS EXAMPLE -# -# This code check if the device at COMx is the expected device. -# The device ID used is the 2080, the IblBehavior - - -# Open the device -# Open serial connection -if os.name == "posix": # check for Linux. - device = Device("/dev/ttyUSB0") -else: # assume Windows. - device = Device("COM95") - -# Get some of the device's parameters -device_id = device.WHO_AM_I # Get device's ID -device_id_description = device.DEFAULT_DEVICE_NAME # Get device's user name -device_user_name = device.DEVICE_NAME # Get device's user name - -# Check if we are dealing with the correct device -if device_id in device_names: - print("Correct device was found!") - print(f"Device's ID: {device_id}") - print(f"Device's default name: {device_id_description}") - print(f"Device's user name: {device_user_name}") -else: - print("Device not correct or is not a Harp device!") - -# Close connection -device.disconnect() diff --git a/docs/examples/code/get_info.py b/docs/examples/code/get_info.py deleted file mode 100755 index 05ed9f9..0000000 --- a/docs/examples/code/get_info.py +++ /dev/null @@ -1,42 +0,0 @@ -import os - -from pyharp.device import Device - -# ON THIS EXAMPLE -# -# This code opens the connection with the device and displays the information -# Also saves device's information into variables - - -# Open the device and print the info on screen -# Open serial connection and save communication to a file -if os.name == "posix": # check for Linux. - # device = Device("/dev/harp_device_00", "ibl.bin") - # device = Device("/dev/ttyACM0") - device = Device("/dev/ttyUSB0") -else: # assume Windows. - device = Device("COM95", "ibl.bin") -device.info() # Display device's info on screen - -# Get some of the device's parameters -device_id = device.WHO_AM_I # Get device's ID -device_id_description = device.DEFAULT_DEVICE_NAME # Get device's user name -device_user_name = device.DEVICE_NAME # Get device's user name - -# Get versions -device_fw_h = device.FIRMWARE_VERSION_H # Get device's firmware version -device_fw_l = device.FIRMWARE_VERSION_L # Get device's firmware version -device_hw_h = device.HW_VERSION_H # Get device's hardware version -device_hw_l = device.HW_VERSION_L # Get device's hardware version -device_harp_h = device.HARP_VERSION_H # Get device's harp core version -device_harp_l = device.HARP_VERSION_L # Get device's harp core version -device_assembly = device.ASSEMBLY_VERSION # Get device's assembly version -device_serial_number = device.SERIAL_NUMBER # Get device's serial number - -reg_dump = device.dump_registers() -for reg_reply in reg_dump: - print(reg_reply) - print() - -# Close connection -device.disconnect() diff --git a/docs/examples/code/wait_for_events.py b/docs/examples/code/wait_for_events.py deleted file mode 100755 index 6cdb615..0000000 --- a/docs/examples/code/wait_for_events.py +++ /dev/null @@ -1,26 +0,0 @@ -import os - -from pyharp.device import Device, DeviceMode -from pyharp.drivers.behavior import Behavior - -# Open the device and print the info on screen -# Open serial connection and save communication to a file -device = None -if os.name == "posix": # check for Linux. - # device = Behavior("/dev/harp_device_00", "ibl.bin") - # device = Device("/dev/ttyACM0",) - device = Device( - "/dev/ttyUSB0", - ) -else: # assume Windows. - device = Behavior("COM95", "ibl.bin") - -print("Setting mode to active.") -# Mode will remain active for up to 3 seconds after CTS pin is brought low. -device.set_mode(DeviceMode.Active) -# device.disable_all_events() -# device.enable_events(Events.port_digital_inputs) -while True: - for msg in device.get_events(): - print(msg) - print() diff --git a/docs/examples/code/write_and_read_from_registers.py b/docs/examples/code/write_and_read_from_registers.py deleted file mode 100755 index 91d7c7e..0000000 --- a/docs/examples/code/write_and_read_from_registers.py +++ /dev/null @@ -1,59 +0,0 @@ -import os - -from pyharp.device import Device - -# ON THIS EXAMPLE -# -# This code opens the connection with the device and update content on a register -# It uses register address 42, which stores the analog sensor's higher threshold in the IBLBehavior device -# This register is unsigned with 16 bits (U16) - - -# Open the device and print the info on screen -# Open serial connection and save communication to a file -if os.name == "posix": # check for Linux. - device = Device("/dev/ttyUSB0", "ibl.bin") -else: # assume Windows. - device = Device("COM95", "ibl.bin") - -# Read current analog sensor's higher threshold (ANA_SENSOR_TH0_HIGH) at address 42 -# analog_threshold_h = device.send(HarpMessage.ReadU16(42).frame).payload_as_int() -# print(f"Analog sensor's higher threshold: {analog_threshold_h}") - - -import time - -print(f"System time: {time.perf_counter():.6f}") -data_stream = device.read_u8(33) - -print(f"Data Stream payload type: {data_stream.payload_type.name}") -print(f"Data Stream message type: {data_stream.message_type.name}") -print(f"Data Stream timestamp: {data_stream.timestamp}") -print(f"Data Stream num bytes: {data_stream.length}") -print(f"Data Stream payload: {data_stream.payload}") - -print(f"System time: {time.perf_counter():.6f}") -event_reg_response = device.read_u8(77) # returns a ReplyHarpMessage -print(f"EVNT_ENABLE payload type: {event_reg_response.payload_type.name}") -print(f"EVNT_ENABLE message type: {event_reg_response.message_type.name}") -print(f"EVNT_ENABLE timestamp: {event_reg_response.timestamp}") -print(f"EVNT_ENABLE num bytes: {event_reg_response.length}") -print(f"EVNT_ENABLE payload: {event_reg_response.payload[0]:08b}") - -## Increase current analog sensor's higher threshold by one unit -# device.send(HarpMessage.WriteU16(42, analog_threshold_h+1).frame) -# -## Check if the register was well written -# analog_threshold_h = device.send(HarpMessage.ReadU16(42).frame).payload_as_int() -# print(f"Analog sensor's higher threshold: {analog_threshold_h}") -# -## Read 10 samples of the analog sensor and display the values -## The value is at register STREAM[0], address 33 -# analog_sensor = [] -# for x in range(10): -# value = device.send(HarpMessage.ReadS16(33).frame).payload_as_int() -# analog_sensor.append(value & 0xffff) -# print(f"Analog sensor's values: {analog_sensor}") - -# Close connection -device.disconnect() diff --git a/docs/examples/get_info.md b/docs/examples/get_info.md deleted file mode 100644 index 96d03e9..0000000 --- a/docs/examples/get_info.md +++ /dev/null @@ -1,9 +0,0 @@ -# Getting Device Info - -This example demonstrates connecting to a Harp device and reading its information: - - -```python -[](./code/get_info.py) -``` - diff --git a/docs/examples/get_info/get_info.md b/docs/examples/get_info/get_info.md new file mode 100644 index 0000000..0ebfe38 --- /dev/null +++ b/docs/examples/get_info/get_info.md @@ -0,0 +1,12 @@ +# Getting Device Info + +This example demonstrates how to connect to a Harp device, read its info and dump the device's registers. + +!!! warning + Don't forget to change the `SERIAL_PORT` to the one that corresponds to your device! The `SERIAL_PORT` must be denoted as `/dev/ttyUSBx` in Linux and `COMx` in Windows, where `x` is the number of the serial port. + + +```python +[](./get_info.py) +``` + diff --git a/docs/examples/get_info/get_info.py b/docs/examples/get_info/get_info.py new file mode 100755 index 0000000..318c307 --- /dev/null +++ b/docs/examples/get_info/get_info.py @@ -0,0 +1,20 @@ +from pyharp.device import Device + +SERIAL_PORT = ( + "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port) +) + +# Open serial connection and save communication to a file +device = Device(SERIAL_PORT, "dump.bin") + +# Display device's info on screen +device.info() + +# Dump device's registers +reg_dump = device.dump_registers() +for reg_reply in reg_dump: + print(reg_reply) + print() + +# Close connection +device.disconnect() diff --git a/docs/examples/index.md b/docs/examples/index.md index 6c4ac5b..73bad13 100644 --- a/docs/examples/index.md +++ b/docs/examples/index.md @@ -5,6 +5,6 @@ This section contains some examples to help you get started with `pyharp`. Here's the complete list of available examples: - [Getting Device Info](get_info.md) - connect to a Harp device and read its information. -- [Check Device ID](check_device.md) - connect to a Harp device and check its ID. - [Wait for Events](wait_for_events.md) - connect to a Harp device and wait for events. -- [Write and Read from Registers](write_and_read_from_registers.md) - connect to a Harp device and read from registers. \ No newline at end of file +- [Write and Read from Registers](write_and_read_from_registers.md) - connect to the Harp Behavior, read from a digital input and write to a digital output. +- [Olfactometer Example](olfactometer_example.md) - connect to the Harp Olfactometer, enable flow, open and close the odor valves and monitor the measured flow values. \ No newline at end of file diff --git a/docs/examples/read_and_write_from_registers/read_and_write_from_registers.md b/docs/examples/read_and_write_from_registers/read_and_write_from_registers.md new file mode 100644 index 0000000..ea819d9 --- /dev/null +++ b/docs/examples/read_and_write_from_registers/read_and_write_from_registers.md @@ -0,0 +1,17 @@ +# Read and Write from Registers + +This example demonstrates how to read and write from registers. In this particular example, the [Harp Behavior](https://harp-tech.org/api/Harp.Behavior.html) is used to read from the DI3 pin and to turn on and off the DO0 pin, according to the schematics shown [below](#schematics). + +!!! warning + Don't forget to change the `SERIAL_PORT` to the one that corresponds to your device! The `SERIAL_PORT` must be denoted as `/dev/ttyUSBx` in Linux and `COMx` in Windows, where `x` is the number of the serial port. + + +```python +[](./read_and_write_from_registers.py) +``` + + +## Schematics + +!!! warning + _TODO_ \ No newline at end of file diff --git a/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py b/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py new file mode 100755 index 0000000..cd8fab0 --- /dev/null +++ b/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py @@ -0,0 +1,33 @@ +from serial import SerialException + +from pyharp import MessageType, PayloadType +from pyharp.device import Device +from pyharp.messages import HarpMessage + +SERIAL_PORT = ( + "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port) +) + +# Open serial connection and save communication to a file +device = Device(SERIAL_PORT, "dump.bin") + +# Check if the device is a Harp Behavior +if not device.WHO_AM_I == 1216: + raise SerialException("This is not a Harp Behavior.") + +# Read initial DI3 state +reply = device.send(HarpMessage.create(MessageType.READ, 32, PayloadType.U8)) +print(reply.payload & 0x08) + +# Turn DO0 on and read DI3 state after it +reply = device.send(HarpMessage.create(MessageType.READ, 34, PayloadType.U8, 0x400)) +reply = device.send(HarpMessage.create(MessageType.READ, 32, PayloadType.U8)) +print(reply.payload & 0x08) + +# Turn DO0 off and read DI3 state again +reply = device.send(HarpMessage.create(MessageType.READ, 35, PayloadType.U8, 0x400)) +reply = device.send(HarpMessage.create(MessageType.READ, 32, PayloadType.U8)) +print(reply.payload & 0x08) + +# Close connection +device.disconnect() diff --git a/docs/examples/wait_for_events.md b/docs/examples/wait_for_events.md deleted file mode 100644 index a40c86a..0000000 --- a/docs/examples/wait_for_events.md +++ /dev/null @@ -1,9 +0,0 @@ -# Wait for Events - -This example demonstrates connecting to a Harp device and waiting for events: - - -```python -[](./code/wait_for_events.py) -``` - \ No newline at end of file diff --git a/docs/examples/wait_for_events/wait_for_events.md b/docs/examples/wait_for_events/wait_for_events.md new file mode 100644 index 0000000..c4aa6a0 --- /dev/null +++ b/docs/examples/wait_for_events/wait_for_events.md @@ -0,0 +1,12 @@ +# Wait for Events + +This example demonstrates how to read the events sent by the Harp device. + +!!! warning + Don't forget to change the `SERIAL_PORT` to the one that corresponds to your device! The `SERIAL_PORT` must be denoted as `/dev/ttyUSBx` in Linux and `COMx` in Windows, where `x` is the number of the serial port. + + +```python +[](./wait_for_events.py) +``` + \ No newline at end of file diff --git a/docs/examples/wait_for_events/wait_for_events.py b/docs/examples/wait_for_events/wait_for_events.py new file mode 100755 index 0000000..b428b60 --- /dev/null +++ b/docs/examples/wait_for_events/wait_for_events.py @@ -0,0 +1,19 @@ +from pyharp import DeviceMode +from pyharp.device import Device + +SERIAL_PORT = ( + "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port) +) + +# Open serial connection and save communication to a file +device = Device(SERIAL_PORT, "dump.bin") + +# Set device to Active Mode +device.set_mode(DeviceMode.Active) +print("Setting mode to active.") + +# Read device's events +while True: + for msg in device.get_events(): + print(msg) + print() diff --git a/docs/examples/write_and_read_from_registers.md b/docs/examples/write_and_read_from_registers.md deleted file mode 100644 index 55f9e64..0000000 --- a/docs/examples/write_and_read_from_registers.md +++ /dev/null @@ -1,9 +0,0 @@ -# Write and Read from registers - -This example demonstrates connecting to a Harp device and writing and reading from registers: - - -```python -[](./code/write_and_read_from_registers.py) -``` - \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 46dace4..647dbea 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -76,10 +76,10 @@ nav: - Home: index.md - Examples: - examples/index.md - - Getting Device Info: examples/get_info.md - - Check Device ID: examples/check_device_id.md - - Wait for Events: examples/wait_for_events.md - - Write and Read from Registers: examples/write_and_read_from_registers.md + - Getting Device Info: examples/get_info/get_info.md + - Wait for Events: examples/wait_for_events/wait_for_events.md + - Read and Write from Registers: examples/read_and_write_from_registers/read_and_write_from_registers.md + - Olfactometer Example: examples/olfactometer_example/olfactometer_example.md - API: - Device: api/device.md - Messages: api/messages.md From c853f05f34954adfd0d85240a8bf57742fdceac6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Grilo?= Date: Sun, 6 Apr 2025 14:26:07 +0100 Subject: [PATCH 082/267] Docs: add olfactometer example --- .../olfactometer_example.md | 14 ++ .../olfactometer_example.py | 124 ++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 docs/examples/olfactometer_example/olfactometer_example.md create mode 100644 docs/examples/olfactometer_example/olfactometer_example.py diff --git a/docs/examples/olfactometer_example/olfactometer_example.md b/docs/examples/olfactometer_example/olfactometer_example.md new file mode 100644 index 0000000..f78d2e6 --- /dev/null +++ b/docs/examples/olfactometer_example/olfactometer_example.md @@ -0,0 +1,14 @@ +# Olfactometer Example + +This example shows how to interface with the [Harp Olfactometer](https://github.com/harp-tech/device.olfactometer). + +In this example, the flows for the different channels are enabled to random flow values, then every odor valve is opened, one at a time every 5 seconds, and finally the flow is disabled before closing the connection with the device. During this time, the actual flows in every channel are being printed out in the terminal. + +!!! warning + Don't forget to change the `SERIAL_PORT` to the one that corresponds to your device! The `SERIAL_PORT` must be denoted as `/dev/ttyUSBx` in Linux and `COMx` in Windows, where `x` is the number of the serial port. + + +```python +[](./olfactometer_example.py) +``` + diff --git a/docs/examples/olfactometer_example/olfactometer_example.py b/docs/examples/olfactometer_example/olfactometer_example.py new file mode 100644 index 0000000..aa8faa0 --- /dev/null +++ b/docs/examples/olfactometer_example/olfactometer_example.py @@ -0,0 +1,124 @@ +import random +import time +from threading import Event, Thread + +from serial import SerialException + +from pyharp import MessageType, PayloadType +from pyharp.device import Device, DeviceMode +from pyharp.messages import HarpMessage + +SERIAL_PORT = ( + "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port) +) + + +def print_events(device, stop_flag): + while not stop_flag.is_set(): + for msg in device.get_events(): + if ( + msg.address == 48 + or msg.address == 49 + or msg.address == 50 + or msg.address == 51 + or msg.address == 52 + ): + print(msg.address - 48) + print(msg.payload[0]) + print() + + +def main(): + # Open connection + device = Device(SERIAL_PORT) + time.sleep(1) + + stop_flag = Event() + + # Check if the device is a Harp Olfactometer + if not device.WHO_AM_I == 1140: + raise SerialException("This is not a Harp Olfactometer.") + + device.set_mode(DeviceMode.Active) + + # Enable flow + device.send(HarpMessage.WriteU8(32, 0x01).frame) + + # Initialize thread for events + events_thread = Thread( + target=print_events, + args=( + device, + stop_flag, + ), + ) + events_thread.start() + + # Set the valves to a random flow + device.send( + HarpMessage.create( + MessageType.WRITE, 42, PayloadType.Float, int(random.random() * 100) + ).frame + ) + device.send( + HarpMessage.create( + MessageType.WRITE, 43, PayloadType.Float, int(random.random() * 100) + ).frame + ) + device.send( + HarpMessage.create( + MessageType.WRITE, 44, PayloadType.Float, int(random.random() * 100) + ).frame + ) + device.send( + HarpMessage.create( + MessageType.WRITE, 45, PayloadType.Float, int(random.random() * 100) + ).frame + ) + + # Open every odor valve, one at a time every 5 seconds + device.send( + HarpMessage.create(MessageType.WRITE, 68, PayloadType.Float, 0x01).frame + ) + time.sleep(5) + device.send( + HarpMessage.create(MessageType.WRITE, 69, PayloadType.Float, 0x01).frame + ) + device.send( + HarpMessage.create(MessageType.WRITE, 68, PayloadType.Float, 0x02).frame + ) + time.sleep(5) + device.send( + HarpMessage.create(MessageType.WRITE, 69, PayloadType.Float, 0x02).frame + ) + device.send( + HarpMessage.create(MessageType.WRITE, 68, PayloadType.Float, 0x04).frame + ) + time.sleep(5) + device.send( + HarpMessage.create(MessageType.WRITE, 69, PayloadType.Float, 0x04).frame + ) + device.send( + HarpMessage.create(MessageType.WRITE, 68, PayloadType.Float, 0x08).frame + ) + time.sleep(5) + device.send( + HarpMessage.create(MessageType.WRITE, 69, PayloadType.Float, 0x08).frame + ) + time.sleep(5) + + # Disable flow + device.send( + HarpMessage.create(MessageType.WRITE, 32, PayloadType.Float, 0x00).frame + ) + time.sleep(1) + + stop_flag.set() + events_thread.join() + + # Close connection + device.disconnect() + + +if __name__ == "__main__": + main() From 5c77aa6af8b10117d998c536541137cec7220859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Grilo?= Date: Sun, 6 Apr 2025 14:26:46 +0100 Subject: [PATCH 083/267] Docs: add pyharp core API to documentation --- docs/api/core.md | 4 ++++ mkdocs.yml | 1 + 2 files changed, 5 insertions(+) create mode 100644 docs/api/core.md diff --git a/docs/api/core.md b/docs/api/core.md new file mode 100644 index 0000000..df5441d --- /dev/null +++ b/docs/api/core.md @@ -0,0 +1,4 @@ +::: pyharp.MessageType +::: pyharp.PayloadType +::: pyharp.CommonRegisters +::: pyharp.DeviceMode \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 647dbea..85e4ba7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -81,6 +81,7 @@ nav: - Read and Write from Registers: examples/read_and_write_from_registers/read_and_write_from_registers.md - Olfactometer Example: examples/olfactometer_example/olfactometer_example.md - API: + - Core: api/core.md - Device: api/device.md - Messages: api/messages.md From 299b4d020b067c715da244b9a46406e7e0664cd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 9 Apr 2025 10:11:12 +0100 Subject: [PATCH 084/267] Change dump file usage convention --- pyharp/device.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/pyharp/device.py b/pyharp/device.py index 27212fd..64695bf 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -2,6 +2,7 @@ import logging import queue +from io import BufferedWriter from pathlib import Path from typing import Optional, Union @@ -57,6 +58,7 @@ class Device: _ser: HarpSerial _dump_file_path: Path + _dump_file: Optional[BufferedWriter] _read_timeout_s: float _TIMEOUT_S: float = 1.0 @@ -79,9 +81,8 @@ def __init__( """ self.log = logging.getLogger(f"{__name__}.{self.__class__.__name__}") self._serial_port = serial_port - if dump_file_path is None: - self._dump_file_path = None - else: + self._dump_file_path = dump_file_path + if dump_file_path is not None: self._dump_file_path = Path() / dump_file_path self._read_timeout_s = read_timeout_s @@ -135,10 +136,19 @@ def connect(self) -> None: rtscts=True, ) + # open file if it is defined + if self._dump_file_path is not None: + self._dump_file = open(self._dump_file_path, "ab") + def disconnect(self) -> None: """ Disconnects from the Harp device. """ + # close file if it exists + if self._dump_file: + self._dump_file.close() + self._dump_file = None + self._ser.close() def _read_device_mode(self) -> DeviceMode: @@ -358,7 +368,7 @@ def send(self, message_bytes: bytearray, dump: bool = True) -> ReplyHarpMessage: # TODO: handle case where read is None reply: ReplyHarpMessage = self._read() - if dump and self._dump_file_path is not None: + if dump: self._dump_reply(reply.frame) return reply @@ -381,10 +391,8 @@ def _dump_reply(self, reply: bytes): """ Dumps the reply to a Harp message in the dump file in case it exists. """ - # TODO: try to handle a None _dump_file_path in a different way - assert self._dump_file_path is not None - with self._dump_file_path.open(mode="ab") as f: - f.write(reply) + if self._dump_file: + self._dump_file.write(reply) def get_events(self) -> list[ReplyHarpMessage]: """ From b924837a4dbdb41ebc18ca21afce30830f61c302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 9 Apr 2025 10:37:37 +0100 Subject: [PATCH 085/267] Change DeviceMode to OperationMode for official Harp documentation coherence --- .../olfactometer_example.py | 4 +-- .../wait_for_events/wait_for_events.py | 4 +-- pyharp/__init__.py | 2 +- pyharp/base.py | 25 +++++++++++++++---- pyharp/device.py | 8 +++--- 5 files changed, 29 insertions(+), 14 deletions(-) diff --git a/docs/examples/olfactometer_example/olfactometer_example.py b/docs/examples/olfactometer_example/olfactometer_example.py index aa8faa0..aea85b1 100644 --- a/docs/examples/olfactometer_example/olfactometer_example.py +++ b/docs/examples/olfactometer_example/olfactometer_example.py @@ -5,7 +5,7 @@ from serial import SerialException from pyharp import MessageType, PayloadType -from pyharp.device import Device, DeviceMode +from pyharp.device import Device, OperationMode from pyharp.messages import HarpMessage SERIAL_PORT = ( @@ -39,7 +39,7 @@ def main(): if not device.WHO_AM_I == 1140: raise SerialException("This is not a Harp Olfactometer.") - device.set_mode(DeviceMode.Active) + device.set_mode(OperationMode.ACTIVE) # Enable flow device.send(HarpMessage.WriteU8(32, 0x01).frame) diff --git a/docs/examples/wait_for_events/wait_for_events.py b/docs/examples/wait_for_events/wait_for_events.py index b428b60..e1064c2 100755 --- a/docs/examples/wait_for_events/wait_for_events.py +++ b/docs/examples/wait_for_events/wait_for_events.py @@ -1,4 +1,4 @@ -from pyharp import DeviceMode +from pyharp import OperationMode from pyharp.device import Device SERIAL_PORT = ( @@ -9,7 +9,7 @@ device = Device(SERIAL_PORT, "dump.bin") # Set device to Active Mode -device.set_mode(DeviceMode.Active) +device.set_mode(OperationMode.ACTIVE) print("Setting mode to active.") # Read device's events diff --git a/pyharp/__init__.py b/pyharp/__init__.py index fc569a5..d40c35b 100644 --- a/pyharp/__init__.py +++ b/pyharp/__init__.py @@ -1,3 +1,3 @@ -from pyharp.base import CommonRegisters, DeviceMode, MessageType, PayloadType +from pyharp.base import CommonRegisters, MessageType, OperationMode, PayloadType __version__ = "0.1.0" diff --git a/pyharp/base.py b/pyharp/base.py index 6962a0e..38e7214 100644 --- a/pyharp/base.py +++ b/pyharp/base.py @@ -148,8 +148,23 @@ class CommonRegisters(IntEnum): SERIAL_NUMBER = 0x0D -class DeviceMode(IntEnum): - Standby = 0 - Active = 1 - Reserved = 2 - Speed = 3 +class OperationMode(IntEnum): + """ + An enumeration with the operation modes of a Harp device. More information on the operation modes can be found [here](https://harp-tech.org/protocol/Device.html#r_operation_ctrl-u16--operation-mode-configuration). + + Attributes + ---------- + STANDBY : int + the value that corresponds to the Standby operation mode (0). The device has all the Events turned off. + ACTIVE : int + the value that corresponds to the Active operation mode (1). The device turns ON the Events detection. Only the enabled Events will be operating. + RESERVED : int + the value that corresponds to the Reserved operation mode (2) + SPEED : int + the value that corresponds to the Speed operation mode (3). The device enters Speed Mode. + """ + + STANDBY = 0 + ACTIVE = 1 + RESERVED = 2 + SPEED = 3 diff --git a/pyharp/device.py b/pyharp/device.py index 64695bf..56af486 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -8,7 +8,7 @@ import serial -from pyharp import CommonRegisters, DeviceMode, MessageType, PayloadType +from pyharp import CommonRegisters, MessageType, OperationMode, PayloadType from pyharp.device_names import device_names from pyharp.harp_serial import HarpSerial from pyharp.messages import HarpMessage, ReplyHarpMessage @@ -151,7 +151,7 @@ def disconnect(self) -> None: self._ser.close() - def _read_device_mode(self) -> DeviceMode: + def _read_device_mode(self) -> OperationMode: """ Reads the current operation mode of the Harp device. @@ -164,7 +164,7 @@ def _read_device_mode(self) -> DeviceMode: reply = self.send( HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame ) - return DeviceMode(reply.payload_as_int() & 0x03) + return OperationMode(reply.payload_as_int() & 0x03) def dump_registers(self) -> list: """ @@ -198,7 +198,7 @@ def dump_registers(self) -> list: break return replies - def set_mode(self, mode: DeviceMode) -> ReplyHarpMessage: + def set_mode(self, mode: OperationMode) -> ReplyHarpMessage: """ Sets the operation mode of the device. From 1d126339272f3a6a8fd5d185d9ad1bf6e7004919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 9 Apr 2025 10:42:03 +0100 Subject: [PATCH 086/267] Change return type of Device's send method --- pyharp/device.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pyharp/device.py b/pyharp/device.py index 56af486..c4ef087 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -347,7 +347,9 @@ def reset_device(self) -> ReplyHarpMessage: return reply - def send(self, message_bytes: bytearray, dump: bool = True) -> ReplyHarpMessage: + def send( + self, message_bytes: bytearray, dump: bool = True + ) -> Optional[ReplyHarpMessage]: """ Sends a Harp message. @@ -360,13 +362,12 @@ def send(self, message_bytes: bytearray, dump: bool = True) -> ReplyHarpMessage: Returns ------- - ReplyHarpMessage - the reply to the Harp message + Optional[ReplyHarpMessage] + the reply to the Harp message or None if no reply is given """ self._ser.write(message_bytes) - # TODO: handle case where read is None - reply: ReplyHarpMessage = self._read() + reply = self._read() if dump: self._dump_reply(reply.frame) From 98046e6bb73cb5b14cfa3d359d58d7e083ee7b62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 9 Apr 2025 11:45:20 +0100 Subject: [PATCH 087/267] Update and add missing OperationControl methods --- pyharp/device.py | 82 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 61 insertions(+), 21 deletions(-) diff --git a/pyharp/device.py b/pyharp/device.py index c4ef087..40c19f0 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -232,14 +232,19 @@ def set_mode(self, mode: OperationMode) -> ReplyHarpMessage: return reply - def enable_status_led(self) -> ReplyHarpMessage: + def alive_en(self, enable: bool) -> bool: """ - Enables the device's status led. + Sets the ALIVE_EN bit of the device. + + Parameters + ---------- + enable : bool + If True, enables the ALIVE_EN bit. If False, disables it. Returns ------- - ReplyHarpMessage - the reply to the Harp message + bool + True if the operation was successful, False otherwise. """ address = CommonRegisters.OPERATION_CTRL @@ -247,7 +252,12 @@ def enable_status_led(self) -> ReplyHarpMessage: reg_value = self.send( HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame ).payload_as_int() - reg_value |= 1 << 5 + + if enable: + reg_value |= 1 << 7 + else: + reg_value &= ~(1 << 7) + reply = self.send( HarpMessage.create( MessageType.WRITE, address, PayloadType.U8, reg_value @@ -256,14 +266,19 @@ def enable_status_led(self) -> ReplyHarpMessage: return reply - def disable_status_led(self) -> ReplyHarpMessage: + def op_led_en(self, enable: bool) -> bool: """ - Disables the device's status led. + Sets the operation LED of the device. + + Parameters + ---------- + enable : bool + If True, enables the operation LED. If False, disables it. Returns ------- - ReplyHarpMessage - the reply to the Harp message + bool + True if the operation was successful, False otherwise. """ address = CommonRegisters.OPERATION_CTRL @@ -271,7 +286,12 @@ def disable_status_led(self) -> ReplyHarpMessage: reg_value = self.send( HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame ).payload_as_int() - reg_value &= ~(1 << 5) + + if enable: + reg_value |= 1 << 6 + else: + reg_value &= ~(1 << 6) + reply = self.send( HarpMessage.create( MessageType.WRITE, address, PayloadType.U8, reg_value @@ -280,14 +300,19 @@ def disable_status_led(self) -> ReplyHarpMessage: return reply - def enable_alive_en(self) -> ReplyHarpMessage: + def status_led(self, enable: bool) -> bool: """ - Enables the ALIVE_EN bit so that the device sends an event each second. More information on the ALIVE_EN bit can be found [here](https://harp-tech.org/protocol/Device.html#r_operation_ctrl-u16--operation-mode-configuration). + Sets the status led of the device. + + Parameters + ---------- + enable : bool + If True, enables the status led. If False, disables it. Returns ------- - ReplyHarpMessage - the reply to the Harp message + bool + True if the operation was successful, False otherwise. """ address = CommonRegisters.OPERATION_CTRL @@ -295,7 +320,12 @@ def enable_alive_en(self) -> ReplyHarpMessage: reg_value = self.send( HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame ).payload_as_int() - reg_value |= 1 << 7 + + if enable: + reg_value |= 1 << 5 + else: + reg_value &= ~(1 << 5) + reply = self.send( HarpMessage.create( MessageType.WRITE, address, PayloadType.U8, reg_value @@ -304,22 +334,32 @@ def enable_alive_en(self) -> ReplyHarpMessage: return reply - def disable_alive_en(self) -> ReplyHarpMessage: + def mute_reply(self, enable: bool) -> bool: """ - Disables the ALIVE_EN bit so that the device does not send an event each second. More information on the ALIVE_EN bit can be found [here](https://harp-tech.org/protocol/Device.html#r_operation_ctrl-u16--operation-mode-configuration). + Sets the MUTE_REPLY bit of the device. + + Parameters + ---------- + enable : bool + If True, the Replies to all the Commands are muted. If False, un-mutes them. Returns ------- - ReplyHarpMessage - the reply to the Harp message + bool + True if the operation was successful, False otherwise. """ address = CommonRegisters.OPERATION_CTRL # Read register first reg_value = self.send( HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame - ).payload[0] - reg_value &= ~(1 << 7) + ).payload_as_int() + + if enable: + reg_value |= 1 << 4 + else: + reg_value &= ~(1 << 4) + reply = self.send( HarpMessage.create( MessageType.WRITE, address, PayloadType.U8, reg_value From 9959cdd1c767682ae5cf35ca789aa6a803438a75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 9 Apr 2025 11:45:43 +0100 Subject: [PATCH 088/267] Update docs links --- docs/examples/index.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/examples/index.md b/docs/examples/index.md index 73bad13..695565a 100644 --- a/docs/examples/index.md +++ b/docs/examples/index.md @@ -4,7 +4,7 @@ This section contains some examples to help you get started with `pyharp`. Here's the complete list of available examples: -- [Getting Device Info](get_info.md) - connect to a Harp device and read its information. -- [Wait for Events](wait_for_events.md) - connect to a Harp device and wait for events. -- [Write and Read from Registers](write_and_read_from_registers.md) - connect to the Harp Behavior, read from a digital input and write to a digital output. -- [Olfactometer Example](olfactometer_example.md) - connect to the Harp Olfactometer, enable flow, open and close the odor valves and monitor the measured flow values. \ No newline at end of file +- [Getting Device Info](./get_info/get_info.md) - connect to a Harp device and read its information. +- [Wait for Events](./wait_for_events/wait_for_events.md) - connect to a Harp device and wait for events. +- [Write and Read from Registers](./read_and_write_from_registers/read_and_write_from_registers.md) - connect to the Harp Behavior, read from a digital input and write to a digital output. +- [Olfactometer Example](./olfactometer_example/olfactometer_example.md) - connect to the Harp Olfactometer, enable flow, open and close the odor valves and monitor the measured flow values. \ No newline at end of file From 61c25629f52b9a1dc2e60ee2d813beaa23f99b7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 9 Apr 2025 16:37:00 +0100 Subject: [PATCH 089/267] Fix doc link --- docs/api/core.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/core.md b/docs/api/core.md index df5441d..e3d8970 100644 --- a/docs/api/core.md +++ b/docs/api/core.md @@ -1,4 +1,4 @@ ::: pyharp.MessageType ::: pyharp.PayloadType ::: pyharp.CommonRegisters -::: pyharp.DeviceMode \ No newline at end of file +::: pyharp.OperationMode \ No newline at end of file From d1ef6e0e616759ed0799298461000308cf6fd697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 9 Apr 2025 16:37:20 +0100 Subject: [PATCH 090/267] Comment device tests --- tests/test_device.py | 184 +++++++++++++++++++++---------------------- 1 file changed, 92 insertions(+), 92 deletions(-) diff --git a/tests/test_device.py b/tests/test_device.py index 3a79b1b..fb85b39 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -1,98 +1,98 @@ -import time +# import time -from pyharp import MessageType, PayloadType -from pyharp.device import Device -from pyharp.messages import HarpMessage, ReplyHarpMessage +# from pyharp import MessageType, PayloadType +# from pyharp.device import Device +# from pyharp.messages import HarpMessage, ReplyHarpMessage -DEFAULT_ADDRESS = 42 +# DEFAULT_ADDRESS = 42 -# FIXME -# def test_create_device() -> None: +# # FIXME +# # def test_create_device() -> None: +# # # open serial connection and load info +# # device = Device("COM74", "dump.bin") +# # assert device._ser.is_open +# # device.info() +# # device.disconnect() +# # assert not device._ser.is_open + + +# def test_read_U8() -> None: # # open serial connection and load info -# device = Device("COM74", "dump.bin") -# assert device._ser.is_open -# device.info() +# device = Device("/dev/ttyUSB0", "dump.bin") + +# # read register 38 +# register: int = 38 +# read_size: int = 35 # TODO: automatically calculate this! + +# reply: ReplyHarpMessage = device.send( +# HarpMessage.create(MessageType.READ, register, PayloadType.U8).frame +# ) +# assert reply is not None +# # assert reply.payload_as_int() == write_value + +# print(reply) +# assert device._dump_file_path.exists() # device.disconnect() -# assert not device._ser.is_open - - -def test_read_U8() -> None: - # open serial connection and load info - device = Device("/dev/ttyUSB0", "dump.bin") - - # read register 38 - register: int = 38 - read_size: int = 35 # TODO: automatically calculate this! - - reply: ReplyHarpMessage = device.send( - HarpMessage.create(MessageType.READ, register, PayloadType.U8).frame - ) - assert reply is not None - # assert reply.payload_as_int() == write_value - - print(reply) - assert device._dump_file_path.exists() - device.disconnect() - - -def test_U8() -> None: - # open serial connection and load info - device = Device("/dev/ttyUSB0", "dump.bin") - assert device._dump_file_path.exists() - - register: int = 38 - read_size: int = 20 # TODO: automatically calculate this! - write_value: int = 65 - - # assert reply[11] == 0 # what is the default register value?! - - # write 65 on register 38 - reply: ReplyHarpMessage = device.send( - HarpMessage.create( - MessageType.WRITE, register, PayloadType.U8, write_value - ).frame - ) - assert reply is not None - - # read register 38 - reply = device.read_u8(register) - assert reply is not None - assert reply.payload_as_int() == write_value - - device.disconnect() - - -# def test_read_hw_version_integration() -> None: -# -# # serial settings -# ser = serial.Serial( -# "/dev/tty.usbserial-A106C8O9", -# baudrate=1000000, -# timeout=5, -# parity=serial.PARITY_NONE, -# stopbits=1, -# bytesize=8, -# rtscts=True, + +# # FIXME: this seems to be testing the Behavior device, not a generic harp device. +# def test_U8() -> None: +# # open serial connection and load info +# device = Device("/dev/ttyUSB0", "dump.bin") +# assert device._dump_file_path.exists() + +# register: int = 38 +# read_size: int = 20 # TODO: automatically calculate this! +# write_value: int = 65 + +# # assert reply[11] == 0 # what is the default register value?! + +# # write 65 on register 38 +# reply = device.send( +# HarpMessage.create( +# MessageType.WRITE, register, PayloadType.U8, write_value +# ).frame # ) -# -# assert ser.is_open -# -# ser.write(b"\x01\x04\x01\xff\x01\x06") # read HW major version (register 1) -# ser.write(b"\x01\x04\x02\xff\x01\x07") # read HW minor version (register 2) -# # print(f"In waiting: <{ser.in_waiting}>") -# -# data = ser.read(100) -# print(f"Data: {data}") -# ser.close() -# assert not ser.is_open -# -# # assert data[0] == '\t' - - -# FIXME -# def test_device_events(device: Device) -> None: -# while True: -# print(device.event_count()) -# for msg in device.get_events(): -# print(msg) -# time.sleep(0.3) +# assert reply is not None + +# # read register 38 +# reply = device.read_u8(register) +# assert reply is not None +# assert reply.payload_as_int() == write_value + +# device.disconnect() + + +# # def test_read_hw_version_integration() -> None: +# # +# # # serial settings +# # ser = serial.Serial( +# # "/dev/tty.usbserial-A106C8O9", +# # baudrate=1000000, +# # timeout=5, +# # parity=serial.PARITY_NONE, +# # stopbits=1, +# # bytesize=8, +# # rtscts=True, +# # ) +# # +# # assert ser.is_open +# # +# # ser.write(b"\x01\x04\x01\xff\x01\x06") # read HW major version (register 1) +# # ser.write(b"\x01\x04\x02\xff\x01\x07") # read HW minor version (register 2) +# # # print(f"In waiting: <{ser.in_waiting}>") +# # +# # data = ser.read(100) +# # print(f"Data: {data}") +# # ser.close() +# # assert not ser.is_open +# # +# # # assert data[0] == '\t' + + +# # FIXME +# # def test_device_events(device: Device) -> None: +# # while True: +# # print(device.event_count()) +# # for msg in device.get_events(): +# # print(msg) +# # time.sleep(0.3) From 439802429ed222db9f703051f160f73d3c50195d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 9 Apr 2025 16:37:41 +0100 Subject: [PATCH 091/267] Add pytest-cov dev dependency --- pyproject.toml | 1 + uv.lock | 109 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index b124dea..7225be3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ dev = [ "mkdocs-material>=9.6.9", "mkdocstrings-python>=1.16.6", "pytest>=8.3.5", + "pytest-cov>=6.1.1", "ruff>=0.11.0", ] diff --git a/uv.lock b/uv.lock index 046433d..9a082b9 100644 --- a/uv.lock +++ b/uv.lock @@ -102,6 +102,61 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] +[[package]] +name = "coverage" +version = "7.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/77/074d201adb8383addae5784cb8e2dac60bb62bfdf28b2b10f3a3af2fda47/coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27", size = 211493 }, + { url = "https://files.pythonhosted.org/packages/a9/89/7a8efe585750fe59b48d09f871f0e0c028a7b10722b2172dfe021fa2fdd4/coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea", size = 211921 }, + { url = "https://files.pythonhosted.org/packages/e9/ef/96a90c31d08a3f40c49dbe897df4f1fd51fb6583821a1a1c5ee30cc8f680/coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7", size = 244556 }, + { url = "https://files.pythonhosted.org/packages/89/97/dcd5c2ce72cee9d7b0ee8c89162c24972fb987a111b92d1a3d1d19100c61/coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040", size = 242245 }, + { url = "https://files.pythonhosted.org/packages/b2/7b/b63cbb44096141ed435843bbb251558c8e05cc835c8da31ca6ffb26d44c0/coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543", size = 244032 }, + { url = "https://files.pythonhosted.org/packages/97/e3/7fa8c2c00a1ef530c2a42fa5df25a6971391f92739d83d67a4ee6dcf7a02/coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2", size = 243679 }, + { url = "https://files.pythonhosted.org/packages/4f/b3/e0a59d8df9150c8a0c0841d55d6568f0a9195692136c44f3d21f1842c8f6/coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318", size = 241852 }, + { url = "https://files.pythonhosted.org/packages/9b/82/db347ccd57bcef150c173df2ade97976a8367a3be7160e303e43dd0c795f/coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9", size = 242389 }, + { url = "https://files.pythonhosted.org/packages/21/f6/3f7d7879ceb03923195d9ff294456241ed05815281f5254bc16ef71d6a20/coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c", size = 213997 }, + { url = "https://files.pythonhosted.org/packages/28/87/021189643e18ecf045dbe1e2071b2747901f229df302de01c998eeadf146/coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78", size = 214911 }, + { url = "https://files.pythonhosted.org/packages/aa/12/4792669473297f7973518bec373a955e267deb4339286f882439b8535b39/coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc", size = 211684 }, + { url = "https://files.pythonhosted.org/packages/be/e1/2a4ec273894000ebedd789e8f2fc3813fcaf486074f87fd1c5b2cb1c0a2b/coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6", size = 211935 }, + { url = "https://files.pythonhosted.org/packages/f8/3a/7b14f6e4372786709a361729164125f6b7caf4024ce02e596c4a69bccb89/coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d", size = 245994 }, + { url = "https://files.pythonhosted.org/packages/54/80/039cc7f1f81dcbd01ea796d36d3797e60c106077e31fd1f526b85337d6a1/coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05", size = 242885 }, + { url = "https://files.pythonhosted.org/packages/10/e0/dc8355f992b6cc2f9dcd5ef6242b62a3f73264893bc09fbb08bfcab18eb4/coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a", size = 245142 }, + { url = "https://files.pythonhosted.org/packages/43/1b/33e313b22cf50f652becb94c6e7dae25d8f02e52e44db37a82de9ac357e8/coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6", size = 244906 }, + { url = "https://files.pythonhosted.org/packages/05/08/c0a8048e942e7f918764ccc99503e2bccffba1c42568693ce6955860365e/coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47", size = 243124 }, + { url = "https://files.pythonhosted.org/packages/5b/62/ea625b30623083c2aad645c9a6288ad9fc83d570f9adb913a2abdba562dd/coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe", size = 244317 }, + { url = "https://files.pythonhosted.org/packages/62/cb/3871f13ee1130a6c8f020e2f71d9ed269e1e2124aa3374d2180ee451cee9/coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545", size = 214170 }, + { url = "https://files.pythonhosted.org/packages/88/26/69fe1193ab0bfa1eb7a7c0149a066123611baba029ebb448500abd8143f9/coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b", size = 214969 }, + { url = "https://files.pythonhosted.org/packages/f3/21/87e9b97b568e223f3438d93072479c2f36cc9b3f6b9f7094b9d50232acc0/coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd", size = 211708 }, + { url = "https://files.pythonhosted.org/packages/75/be/882d08b28a0d19c9c4c2e8a1c6ebe1f79c9c839eb46d4fca3bd3b34562b9/coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00", size = 211981 }, + { url = "https://files.pythonhosted.org/packages/7a/1d/ce99612ebd58082fbe3f8c66f6d8d5694976c76a0d474503fa70633ec77f/coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64", size = 245495 }, + { url = "https://files.pythonhosted.org/packages/dc/8d/6115abe97df98db6b2bd76aae395fcc941d039a7acd25f741312ced9a78f/coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067", size = 242538 }, + { url = "https://files.pythonhosted.org/packages/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008", size = 244561 }, + { url = "https://files.pythonhosted.org/packages/22/70/c10c77cd77970ac965734fe3419f2c98665f6e982744a9bfb0e749d298f4/coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733", size = 244633 }, + { url = "https://files.pythonhosted.org/packages/38/5a/4f7569d946a07c952688debee18c2bb9ab24f88027e3d71fd25dbc2f9dca/coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323", size = 242712 }, + { url = "https://files.pythonhosted.org/packages/bb/a1/03a43b33f50475a632a91ea8c127f7e35e53786dbe6781c25f19fd5a65f8/coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3", size = 244000 }, + { url = "https://files.pythonhosted.org/packages/6a/89/ab6c43b1788a3128e4d1b7b54214548dcad75a621f9d277b14d16a80d8a1/coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d", size = 214195 }, + { url = "https://files.pythonhosted.org/packages/12/12/6bf5f9a8b063d116bac536a7fb594fc35cb04981654cccb4bbfea5dcdfa0/coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487", size = 214998 }, + { url = "https://files.pythonhosted.org/packages/2a/e6/1e9df74ef7a1c983a9c7443dac8aac37a46f1939ae3499424622e72a6f78/coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25", size = 212541 }, + { url = "https://files.pythonhosted.org/packages/04/51/c32174edb7ee49744e2e81c4b1414ac9df3dacfcb5b5f273b7f285ad43f6/coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42", size = 212767 }, + { url = "https://files.pythonhosted.org/packages/e9/8f/f454cbdb5212f13f29d4a7983db69169f1937e869a5142bce983ded52162/coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502", size = 256997 }, + { url = "https://files.pythonhosted.org/packages/e6/74/2bf9e78b321216d6ee90a81e5c22f912fc428442c830c4077b4a071db66f/coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1", size = 252708 }, + { url = "https://files.pythonhosted.org/packages/92/4d/50d7eb1e9a6062bee6e2f92e78b0998848a972e9afad349b6cdde6fa9e32/coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4", size = 255046 }, + { url = "https://files.pythonhosted.org/packages/40/9e/71fb4e7402a07c4198ab44fc564d09d7d0ffca46a9fb7b0a7b929e7641bd/coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73", size = 256139 }, + { url = "https://files.pythonhosted.org/packages/49/1a/78d37f7a42b5beff027e807c2843185961fdae7fe23aad5a4837c93f9d25/coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a", size = 254307 }, + { url = "https://files.pythonhosted.org/packages/58/e9/8fb8e0ff6bef5e170ee19d59ca694f9001b2ec085dc99b4f65c128bb3f9a/coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883", size = 255116 }, + { url = "https://files.pythonhosted.org/packages/56/b0/d968ecdbe6fe0a863de7169bbe9e8a476868959f3af24981f6a10d2b6924/coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada", size = 214909 }, + { url = "https://files.pythonhosted.org/packages/87/e9/d6b7ef9fecf42dfb418d93544af47c940aa83056c49e6021a564aafbc91f/coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257", size = 216068 }, + { url = "https://files.pythonhosted.org/packages/c4/f1/1da77bb4c920aa30e82fa9b6ea065da3467977c2e5e032e38e66f1c57ffd/coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd", size = 203443 }, + { url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + [[package]] name = "ghp-import" version = "2.1.0" @@ -470,6 +525,7 @@ dev = [ { name = "mkdocs-material" }, { name = "mkdocstrings-python" }, { name = "pytest" }, + { name = "pytest-cov" }, { name = "ruff" }, ] @@ -485,6 +541,7 @@ dev = [ { name = "mkdocs-material", specifier = ">=9.6.9" }, { name = "mkdocstrings-python", specifier = ">=1.16.6" }, { name = "pytest", specifier = ">=8.3.5" }, + { name = "pytest-cov", specifier = ">=6.1.1" }, { name = "ruff", specifier = ">=0.11.0" }, ] @@ -525,6 +582,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, ] +[[package]] +name = "pytest-cov" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841 }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -642,6 +712,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303 }, ] +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + [[package]] name = "urllib3" version = "2.3.0" From 9c04d7281e61af27544a4bfc26455272d45a0f71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 9 Apr 2025 16:38:11 +0100 Subject: [PATCH 092/267] Add is_error to HarpMessage --- pyharp/device.py | 2 +- pyharp/messages.py | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/pyharp/device.py b/pyharp/device.py index 40c19f0..dc71690 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -652,7 +652,7 @@ def _read_serial_number(self) -> int: dump=False, ) - if reply.has_error(): + if reply.is_error(): return 0 return reply.payload_as_int() diff --git a/pyharp/messages.py b/pyharp/messages.py index 526948d..9b469a5 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -298,6 +298,18 @@ def __str__(self) -> str: + f"Checksum: {self.checksum}" ) + @property + def is_error(self) -> bool: + """ + Indicates if this HarpMessage is an error message or not. + + Returns + ------- + bool + Returns True if this HarpMessage is an error message, False otherwise. + """ + return self.message_type in [MessageType.READ_ERROR, MessageType.WRITE_ERROR] + # TODO: handle float case @property def payload(self) -> Union[int, list[int]]: @@ -425,7 +437,7 @@ def __init__( value : int, float, List[int], or List[float], optional Value(s) to write - can be a single value or list of values - Notes + Note ----- The message frame is constructed according to the HARP binary protocol. The length is calculated as BASE_LENGTH + payload size in bytes. From 960ce9c77cbd75214e2de6d35553fc7d071ff5ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 9 Apr 2025 16:38:40 +0100 Subject: [PATCH 093/267] Read Timestamp entry for PayloadType --- pyharp/base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyharp/base.py b/pyharp/base.py index 38e7214..c3a0750 100644 --- a/pyharp/base.py +++ b/pyharp/base.py @@ -56,6 +56,8 @@ class PayloadType(IntEnum): the value that corresponds to a message of type S64 Float : PayloadType the value that corresponds to a message of type Float + Timestamp: PayloadType + the value that corresponds to a message of type Timestamp. This is not a valid PayloadType, but it is used to indicate that the message has a timestamp. TimestampedU8 : PayloadType the value that corresponds to a message of type TimestampedU8 TimestampedS8 : PayloadType @@ -85,6 +87,7 @@ class PayloadType(IntEnum): U64 = _isUnsigned | 8 S64 = _isSigned | 8 Float = _isFloat | 4 + Timestamp = _hasTimestamp TimestampedU8 = _hasTimestamp | U8 TimestampedS8 = _hasTimestamp | S8 TimestampedU16 = _hasTimestamp | U16 From bfcb9ecdfe9a07ee5b1018438bb8de8de87caf7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 16 Apr 2025 11:38:46 +0100 Subject: [PATCH 094/267] Handle HarpMessage with arrays and timestamps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also moved the common properties to HarpMessage Co-authored-by: José Grilo --- pyharp/messages.py | 310 ++++++++++++++------------ pyproject.toml | 8 + tests/test_messages.py | 483 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 656 insertions(+), 145 deletions(-) diff --git a/pyharp/messages.py b/pyharp/messages.py index 9b469a5..cc03b85 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -29,6 +29,7 @@ class HarpMessage: """ DEFAULT_PORT: int = 255 + BASE_LENGTH: int = 4 _frame: bytearray def __init__(self): @@ -120,6 +121,135 @@ def payload_type(self) -> PayloadType: """ return PayloadType(self._frame[4]) + @property + def payload(self) -> Union[int, list[int]]: + """ + The payload sent in the write Harp message. + + Returns + ------- + Union[int, list[int]] + the payload sent in the write Harp message + """ + payload_start = self.BASE_LENGTH + if self.payload_type & PayloadType.Timestamp: + payload_start += 6 + + # length is payload_start + payload type size + match self.payload_type: + case PayloadType.U8 | PayloadType.TimestampedU8: + if self.length == payload_start + 1: + return self._frame[5] + else: # array case + return [ + int.from_bytes([self._frame[i]], byteorder="little") + for i in range(5, self.length + 1) + ] + + case PayloadType.S8 | PayloadType.TimestampedS8: + if self.length == payload_start + 1: + return int.from_bytes( + [self._frame[5]], byteorder="little", signed=True + ) + else: # array case + return [ + int.from_bytes( + [self._frame[i]], byteorder="little", signed=True + ) + for i in range(5, self.length + 1) + ] + + case PayloadType.U16 | PayloadType.TimestampedU16: + if self.length == payload_start + 2: + return int.from_bytes( + self._frame[5:7], byteorder="little", signed=False + ) + else: # array case + return [ + int.from_bytes( + self._frame[i : i + 2], byteorder="little", signed=False + ) + for i in range(5, self.length + 1, 2) + ] + + case PayloadType.S16 | PayloadType.TimestampedS16: + if self.length == payload_start + 2: + return int.from_bytes( + self._frame[5:7], byteorder="little", signed=True + ) + else: + return [ + int.from_bytes( + self._frame[i : i + 2], byteorder="little", signed=True + ) + for i in range(5, self.length + 1, 2) + ] + + case PayloadType.U32 | PayloadType.TimestampedU32: + if self.length == payload_start + 4: + return int.from_bytes( + self._frame[5:9], byteorder="little", signed=False + ) + else: + return [ + int.from_bytes( + self._frame[i : i + 4], byteorder="little", signed=False + ) + for i in range(5, self.length + 1, 4) + ] + + case PayloadType.S32 | PayloadType.TimestampedS32: + if self.length == payload_start + 4: + return int.from_bytes( + self._frame[5:9], byteorder="little", signed=True + ) + else: + return [ + int.from_bytes( + self._frame[i : i + 4], byteorder="little", signed=True + ) + for i in range(5, self.length + 1, 4) + ] + + case PayloadType.U64 | PayloadType.TimestampedU64: + if self.length == payload_start + 8: + return int.from_bytes( + self._frame[5:13], byteorder="little", signed=False + ) + else: + return [ + int.from_bytes( + self._frame[i : i + 8], byteorder="little", signed=False + ) + for i in range(5, self.length + 1, 8) + ] + + case PayloadType.S64 | PayloadType.TimestampedS64: + if self.length == payload_start + 8: + return int.from_bytes( + self._frame[5:13], byteorder="little", signed=True + ) + else: + return [ + int.from_bytes( + self._frame[i : i + 8], byteorder="little", signed=True + ) + for i in range(5, self.length + 1, 8) + ] + + case PayloadType.Float | PayloadType.TimestampedFloat: + if self.length == payload_start + 4: + return struct.unpack(" int: """ @@ -183,79 +313,6 @@ def create( "The value cannot be None is message type is equal to MessageType.WRITE!" ) - -class ReplyHarpMessage(HarpMessage): - """ - A response message from a Harp device. - - Attributes - ---------- - payload : Union[int, list[int]] - the message payload formatted as the appropriate type - timestamp : float - the Harp timestamp at which the message was sent - """ - - def __init__( - self, - frame: bytearray, - ): - """ - Parameters - ---------- - frame : bytearray - the Harp message in bytearray format - """ - - self._frame = frame - # Retrieve all content from 11 (where payload starts) until the checksum (not inclusive) - self._raw_payload = frame[11:-1] - - # Format payload as list[PayloadType] - self._payload = self._parse_payload(self._raw_payload) - - # Assign timestamp after _payload since @properties all rely on self._payload. - self._timestamp = ( - int.from_bytes(frame[5:9], byteorder="little", signed=False) - + int.from_bytes(frame[9:11], byteorder="little", signed=False) * 32e-6 - ) - - # Timestamp is junk if it's not present. - if not (self.payload_type & PayloadType.Timestamp): - self._timestamp = None - - # TODO: handle the case of the payload being of type float - def _parse_payload(self, raw_payload: bytearray) -> list[int]: - """ - Returns the payload as a list of ints after parsing it from the raw payload. - - Parameters - ---------- - raw_payload : bytearray - the bytearray containing the raw payload - - Returns - ------- - list[int] - the list of ints containing the payload - """ - is_signed = True if (self.payload_type & 0x80) else False - is_float = True if (self.payload_type & 0x40) else False - bytes_per_word = self.payload_type & 0x07 - payload_len = len(raw_payload) - - word_chunks = [ - raw_payload[i : i + bytes_per_word] - for i in range(0, payload_len, bytes_per_word) - ] - if not is_float: - return [ - int.from_bytes(chunk, byteorder="little", signed=is_signed) - for chunk in word_chunks - ] - else: # TODO: handle float case - return [struct.unpack(" str: """ Prints debug representation of the reply message. @@ -269,12 +326,12 @@ def __repr__(self) -> str: def __str__(self) -> str: """ - Prints friendly representation of the reply message. + Prints friendly representation of a Harp message. Returns ------- str - the representation of the reply message + the representation of the Harp message """ payload_str = "" format_str = "" @@ -284,7 +341,10 @@ def __str__(self) -> str: bytes_per_word = self.payload_type & 0x07 format_str = f"0{bytes_per_word}b" - payload_str = "".join(f"{item:{format_str}} " for item in self.payload) + payload_str = "".join( + f"{item:{format_str}} " + for item in (self.payload if self.payload is list else [self.payload]) + ) return ( f"Type: {self.message_type.name}\r\n" @@ -293,11 +353,49 @@ def __str__(self) -> str: + f"Port: {self.port}\r\n" + f"Timestamp: {self.timestamp}\r\n" + f"Payload Type: {self.payload_type.name}\r\n" - + f"Payload Length: {len(self.payload)}\r\n" + + f"Payload Length: {len(self.payload) if self.payload is list else 1}\r\n" + f"Payload: {payload_str}\r\n" + f"Checksum: {self.checksum}" ) + +class ReplyHarpMessage(HarpMessage): + """ + A response message from a Harp device. + + Attributes + ---------- + payload : Union[int, list[int]] + the message payload formatted as the appropriate type + timestamp : float + the Harp timestamp at which the message was sent + """ + + def __init__( + self, + frame: bytearray, + ): + """ + Parameters + ---------- + frame : bytearray + the Harp message in bytearray format + """ + + self._frame = frame + # Retrieve all content from 11 (where payload starts) until the checksum (not inclusive) + self._raw_payload = frame[11:-1] + + # Assign timestamp after _payload since @properties all rely on self._payload. + self._timestamp = ( + int.from_bytes(frame[5:9], byteorder="little", signed=False) + + int.from_bytes(frame[9:11], byteorder="little", signed=False) * 32e-6 + ) + + # Timestamp is junk if it's not present. + if not (self.payload_type & PayloadType.Timestamp): + self._timestamp = None + @property def is_error(self) -> bool: """ @@ -310,19 +408,6 @@ def is_error(self) -> bool: """ return self.message_type in [MessageType.READ_ERROR, MessageType.WRITE_ERROR] - # TODO: handle float case - @property - def payload(self) -> Union[int, list[int]]: - """ - The message payload formatted as the appropriate type. - - Returns - ------- - Union[int, list[int]] - the message payload formatted as the appropriate type - """ - return self._payload - @property def timestamp(self) -> float: """ @@ -345,7 +430,7 @@ def payload_as_int(self) -> int: int the payload parsed as an int """ - return self.payload[0] + return self._raw_payload[0] def payload_as_string(self) -> str: """ @@ -401,8 +486,6 @@ class WriteHarpMessage(HarpMessage): the payload sent in the write Harp message """ - BASE_LENGTH: int = 5 - BASE_LENGTH: int = 4 MESSAGE_TYPE: int = MessageType.WRITE # Define payload type properties @@ -469,44 +552,3 @@ def __init__( self._frame.append(payload_type) self._frame += payload self._frame.append(self.calculate_checksum()) - - # TODO: handle float and array cases - @property - def payload(self) -> Union[int, list[int]]: - """ - The payload sent in the write Harp message. - - Returns - ------- - Union[int, list[int]] - the payload sent in the write Harp message - """ - match self.payload_type: - case PayloadType.U8: - return self._frame[5] - case PayloadType.S8: - return int.from_bytes([self.frame[5]], byteorder="little", signed=True) - case PayloadType.U16: - return int.from_bytes( - self._frame[5:7], byteorder="little", signed=False - ) - case PayloadType.S16: - return int.from_bytes(self._frame[5:7], byteorder="little", signed=True) - case PayloadType.Float: - return struct.unpack(" None: message = ReadHarpMessage(payload_type=PayloadType.U8, address=DEFAULT_ADDRESS) assert message.message_type == MessageType.READ assert message.checksum == 47 # 1 + 4 + 42 + 255 + 1 - 256 - print(message.frame) def test_create_read_S8() -> None: @@ -17,7 +115,6 @@ def test_create_read_S8() -> None: assert message.message_type == MessageType.READ assert message.checksum == 175 # 1 + 4 + 42 + 255 + 129 - 256 - print(message.frame) def test_create_read_U16() -> None: @@ -25,7 +122,6 @@ def test_create_read_U16() -> None: assert message.message_type == MessageType.READ assert message.checksum == 48 # 1 + 4 + 42 + 255 + 2 - 256 - print(message.frame) def test_create_read_S16() -> None: @@ -33,7 +129,41 @@ def test_create_read_S16() -> None: assert message.message_type == MessageType.READ assert message.checksum == 176 # 1 + 4 + 42 + 255 + 130 - 256 - print(message.frame) + + +def test_create_read_U32() -> None: + message = ReadHarpMessage(payload_type=PayloadType.U32, address=DEFAULT_ADDRESS) + + assert message.message_type == MessageType.READ + assert message.checksum == 50 # 1 + 4 + 42 + 255 + 4 - 256 + + +def test_create_read_S32() -> None: + message = ReadHarpMessage(payload_type=PayloadType.S32, address=DEFAULT_ADDRESS) + + assert message.message_type == MessageType.READ + assert message.checksum == 178 # 1 + 4 + 42 + 255 + 130 - 256 + + +def test_create_read_U64() -> None: + message = ReadHarpMessage(payload_type=PayloadType.U64, address=DEFAULT_ADDRESS) + + assert message.message_type == MessageType.READ + assert message.checksum == 54 # 1 + 4 + 42 + 255 + 2 - 256 + + +def test_create_read_S64() -> None: + message = ReadHarpMessage(payload_type=PayloadType.S64, address=DEFAULT_ADDRESS) + + assert message.message_type == MessageType.READ + assert message.checksum == 182 # 1 + 4 + 42 + 255 + 130 - 256 + + +def test_create_read_float() -> None: + message = ReadHarpMessage(payload_type=PayloadType.Float, address=DEFAULT_ADDRESS) + + assert message.message_type == MessageType.READ + assert message.checksum == 114 # 1 + 4 + 42 + 255 + 4 - 256 def test_create_write_U8() -> None: @@ -42,8 +172,7 @@ def test_create_write_U8() -> None: assert message.message_type == MessageType.WRITE assert message.payload == value - assert message.checksum == 72 # 2 + 5 + 42 + 255 + 1 + 23 - 256 - print(message.frame) + assert message.checksum == 72 # 2 + 4 + 42 + 255 + 1 + 23 = 328 → 328 % 256 = 73 def test_create_write_S8() -> None: @@ -53,7 +182,6 @@ def test_create_write_S8() -> None: assert message.message_type == MessageType.WRITE assert message.payload == value assert message.checksum == 174 # (2 + 5 + 42 + 255 + 129 + 253) & 255 - print(message.frame) def test_create_write_U16() -> None: @@ -64,7 +192,6 @@ def test_create_write_U16() -> None: assert message.length == 6 assert message.payload == value assert message.checksum == 55 # (2 + 6 + 42 + 255 + 2 + 4 + 0) & 255 - print(message.frame) def test_create_write_S16() -> None: @@ -75,7 +202,115 @@ def test_create_write_S16() -> None: assert message.length == 6 assert message.payload == value assert message.checksum == 187 # (2 + 6 + 42 + 255 + 130 + 27 + 237) & 255 - print(message.frame) + + +def test_create_write_U8_array() -> None: + values: list[int] = [1, 2, 3, 4, 5] + message = WriteHarpMessage(PayloadType.U8, DEFAULT_ADDRESS, values) + + assert message.message_type == MessageType.WRITE + assert message.length == 4 + len( + values + ) # 7 header bytes + len(values) payload bytes + assert message.payload == values + assert message.checksum == 68 # (2 + (4 + 5) + 42 + 255 + 1 + 5) & 255 + + +def test_create_write_S8_array() -> None: + values: list[int] = [-1, -2, -3, -4, -5] + message = WriteHarpMessage(PayloadType.S8, DEFAULT_ADDRESS, values) + + assert message.message_type == MessageType.WRITE + assert message.length == 4 + len( + values + ) # 7 header bytes + len(values) payload bytes + assert message.payload == values + assert message.checksum == 166 # (2 + (4 + 5) + 42 + 255 + 129 + 5) & 255 + + +def test_create_write_U16_array() -> None: + values: list[int] = [1, 2, 3, 4, 5] + message = WriteHarpMessage(PayloadType.U16, DEFAULT_ADDRESS, values) + + assert message.message_type == MessageType.WRITE + assert ( + message.length == 4 + len(values) * 2 + ) # 7 header bytes + len(values) * 2 payload bytes + assert message.payload == values + assert message.checksum == 74 # (2 + (4 + 5 * 2) + 42 + 255 + 2 + 5) & 255 + + +def test_create_write_S16_array() -> None: + values: list[int] = [-1, -2, -3, -4, -5] + message = WriteHarpMessage(PayloadType.S16, DEFAULT_ADDRESS, values) + + assert message.message_type == MessageType.WRITE + assert ( + message.length == 4 + len(values) * 2 + ) # 7 header bytes + len(values) * 2 payload bytes + assert message.payload == values + assert message.checksum == 167 # (2 + (4 + 5) + 42 + 255 + 129 + 5) & 255 + + +def test_create_write_U32_array() -> None: + values: list[int] = [1, 2, 3, 4, 5] + message = WriteHarpMessage(PayloadType.U32, DEFAULT_ADDRESS, values) + + assert message.message_type == MessageType.WRITE + assert ( + message.length == 4 + len(values) * 4 + ) # 7 header bytes + len(values) * 4 payload bytes + assert message.payload == values + assert message.checksum == 86 # (2 + (4 + 5 * 4) + 42 + 255 + 4 + 5) & 255 + + +def test_create_write_S32_array() -> None: + values: list[int] = [-1, -2, -3, -4, -5] + message = WriteHarpMessage(PayloadType.S32, DEFAULT_ADDRESS, values) + + assert message.message_type == MessageType.WRITE + assert ( + message.length == 4 + len(values) * 4 + ) # 7 header bytes + len(values) * 4 payload bytes + assert message.payload == values + assert message.checksum == 169 # (2 + (4 + 5 * 4) + 42 + 255 + 130 + 5) & 255 + + +def test_create_write_U64_array() -> None: + values: list[int] = [1, 2, 3, 4, 5] + message = WriteHarpMessage(PayloadType.U64, DEFAULT_ADDRESS, values) + + assert message.message_type == MessageType.WRITE + assert ( + message.length == 4 + len(values) * 8 + ) # 7 header bytes + len(values) * 8 payload bytes + assert message.payload == values + assert message.checksum == 110 # (2 + (4 + 5 * 8) + 42 + 255 + 2 + 5) & 255 + + +def test_create_write_S64_array() -> None: + values: list[int] = [-1, -2, -3, -4, -5] + message = WriteHarpMessage(PayloadType.S64, DEFAULT_ADDRESS, values) + + assert message.message_type == MessageType.WRITE + assert ( + message.length == 4 + len(values) * 8 + ) # 7 header bytes + len(values) * 8 payload bytes + assert message.payload == values + assert message.checksum == 173 # (2 + (4 + 5 * 8) + 42 + 255 + 130 + 5) & 255 + + +def test_create_write_float_array() -> None: + """Test creating a write message with float array values.""" + values = [1.1, 2.2, 3.3] + message = WriteHarpMessage(PayloadType.Float, DEFAULT_ADDRESS, values) + + assert message.message_type == MessageType.WRITE + expected_checksum = 193 # (2 + 4 + 42 + 255 + 1 + 3 * 4) & 255 + assert len(message.payload) == len(values) + for actual, expected in zip(message.payload, values): + assert abs(actual - expected) < 0.0001 # Float comparison with error margin + assert message.checksum == expected_checksum def test_read_who_am_i() -> None: @@ -84,3 +319,229 @@ def test_read_who_am_i() -> None: ) assert str(message.frame) == str(bytearray(b"\x01\x04\x00\xff\x02\x06")) + + +def test_create_write_U32() -> None: + """Test creating a write message with S32 value.""" + value: int = 2147483000 # Large number + message = WriteHarpMessage(PayloadType.U32, DEFAULT_ADDRESS, value) + + assert message.message_type == MessageType.WRITE + assert message.length == 8 + assert message.payload == value + assert len(message.frame) == 10 # length + checksum byte + # Calculate checksum as in other tests + expected_checksum = 42 # (2 + 8 + 42 + 255 + 4 + 0 + 0 + 0 + 0) & 255 + assert message.checksum == expected_checksum + + +def test_create_write_S32() -> None: + """Test creating a write message with S32 value.""" + value: int = -2147483000 # Large negative number + message = WriteHarpMessage(PayloadType.S32, DEFAULT_ADDRESS, value) + + assert message.message_type == MessageType.WRITE + assert message.length == 8 + assert message.payload == value + assert len(message.frame) == 10 # length + checksum byte + # Calculate checksum as in other tests + expected_checksum = 193 # (2 + 8 + 42 + 255 + 130 + 0 + 0 + 0 + 0) & 255 + assert message.checksum == expected_checksum + + +def test_create_write_U64() -> None: + """Test creating a write message with U64 value.""" + value: int = 9223372036854775807 # Large 64-bit value + message = WriteHarpMessage(PayloadType.U64, DEFAULT_ADDRESS, value) + + assert message.message_type == MessageType.WRITE + assert message.length == 12 # 5 header bytes + 8 payload bytes + assert message.payload == value + assert len(message.frame) == 14 # length + checksum byte + # Calculate checksum for 64-bit value + expected_checksum = 183 # (2 + 12 + 42 + 255 + 2 + 0 + 0 + 0 + 0) & 255 + assert message.checksum == expected_checksum + + +def test_create_write_S64() -> None: + """Test creating a write message with S64 value.""" + value: int = -9223372036854775807 + message = WriteHarpMessage(PayloadType.S64, DEFAULT_ADDRESS, value) + assert message.message_type == MessageType.WRITE + assert message.length == 12 + assert message.payload == value + assert len(message.frame) == 14 # length + checksum byte + # Calculate checksum for 64-bit signed value + expected_checksum = 64 # (2 + 12 + 42 + 255 + 130 + 0 + 0 + 0 + 0) & 255 + assert message.checksum == expected_checksum + + +def test_reply_message_str_repr() -> None: + """Test string representation of Reply message.""" + # Create a simple reply message + frame = bytearray( + [ + MessageType.READ, + 5, + 42, + 255, + PayloadType.U8, + 0, + 0, + 0, + 0, # timestamp seconds + 0, + 0, # timestamp micros + 123, # payload + 0, + ] + ) # checksum placeholder + + # Fix checksum + checksum = sum(frame[:-1]) & 255 + frame[-1] = checksum + + reply = ReplyHarpMessage(frame) + str_repr = str(reply) + repr_str = repr(reply) + + assert "Type: READ" in str_repr + assert "Length: 5" in str_repr + assert "Address: 42" in str_repr + assert "Port: 255" in str_repr + assert "Payload: " in str_repr + assert "Raw Frame" in repr_str + + +def test_payload_as_string() -> None: + """Test ReplyHarpMessage.payload_as_string().""" + test_string = "Hello" + encoded = test_string.encode("utf-8") + + frame = bytearray( + [ + MessageType.READ, + 5 + len(encoded), + 42, + 255, + PayloadType.U8, + 0, + 0, + 0, + 0, # timestamp seconds + 0, + 0, + ] + ) # timestamp micros + + # Add string payload + frame.extend(encoded) + # Add checksum + frame.append(0) # placeholder + + # Fix checksum + checksum = sum(frame[:-1]) & 255 + frame[-1] = checksum + + reply = ReplyHarpMessage(frame) + assert reply.payload_as_string() == test_string + + +def test_harp_message_parse() -> None: + """Test the static parse method of HarpMessage.""" + frame = bytearray( + [ + MessageType.READ, + 5, + 42, + 255, + PayloadType.U8, + 0, + 0, + 0, + 0, # timestamp seconds + 0, + 0, # timestamp micros + 123, # payload + 0, + ] + ) # checksum placeholder + + # Fix checksum + checksum = sum(frame[:-1]) & 255 + frame[-1] = checksum + + message = HarpMessage.parse(frame) + assert isinstance(message, ReplyHarpMessage) + assert message.message_type == MessageType.READ + assert message.address == 42 + assert message.payload_as_int() == 123 + + +def test_timestamp_handling() -> None: + """Test timestamp handling in ReplyHarpMessage.""" + # Create a timestamped message + frame = bytearray( + [ + MessageType.READ, + 5, + 42, + 255, + PayloadType.TimestampedU8, + 1, + 0, + 0, + 0, # timestamp seconds = 1 + 32, + 0, # timestamp micros = 32 (= 1ms) + 123, # payload + 0, + ] + ) # checksum placeholder + + # Fix checksum + checksum = sum(frame[:-1]) & 255 + frame[-1] = checksum + + reply = ReplyHarpMessage(frame) + assert reply.timestamp is not None + assert reply.timestamp == 1 + 32 * 32e-6 # 1 second + 1 millisecond + + # Create a non-timestamped message + frame = bytearray( + [ + MessageType.READ, + 5, + 42, + 255, + PayloadType.U8, + 1, + 0, + 0, + 0, # timestamp seconds = 1 + 32, + 0, # timestamp micros = 32 (= 1ms) + 123, # payload + 0, + ] + ) # checksum placeholder + + # Fix checksum + checksum = sum(frame[:-1]) & 255 + frame[-1] = checksum + + reply = ReplyHarpMessage(frame) + assert reply.timestamp is None + + +def test_calculate_checksum() -> None: + """Test the calculate_checksum method.""" + message = HarpMessage() + message._frame = bytearray([1, 2, 3, 4, 5]) + + # Sum is 15, checksum is 15 (no overflow) + assert message.calculate_checksum() == 15 + + message._frame = bytearray([200, 100, 50, 20, 10]) + # Sum is 380, checksum is 380 % 256 = 124 + assert message.calculate_checksum() == 124 From 7223e0c52e89591fdbf988a8fcea50882daede42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 23 Apr 2025 10:52:54 +0100 Subject: [PATCH 095/267] Restore read and write methods from Device MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- pyharp/device.py | 423 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 423 insertions(+) diff --git a/pyharp/device.py b/pyharp/device.py index dc71690..355ab6d 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -463,6 +463,429 @@ def event_count(self) -> int: """ return self._ser.event_q.qsize() + def read_u8(self, address: int) -> ReplyHarpMessage: + """ + Reads the value of a register of type U8. + + Parameters + ---------- + address : int + the register to be read + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message that will contain the value read from the register + """ + return self.send( + HarpMessage.create( + message_type=MessageType.READ, + address=address, + payload_type=PayloadType.U8, + ).frame + ) + + def read_s8(self, address: int) -> ReplyHarpMessage: + """ + Reads the value of a register of type S8. + + Parameters + ---------- + address : int + the register to be read + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message that will contain the value read from the register + """ + return self.send( + HarpMessage.create( + message_type=MessageType.READ, + address=address, + payload_type=PayloadType.S8, + ).frame + ) + + def read_u16(self, address: int) -> ReplyHarpMessage: + """ + Reads the value of a register of type U16. + + Parameters + ---------- + address : int + the register to be read + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message that will contain the value read from the register + """ + return self.send( + HarpMessage.create( + message_type=MessageType.READ, + address=address, + payload_type=PayloadType.U16, + ).frame + ) + + def read_s16(self, address: int) -> ReplyHarpMessage: + """ + Reads the value of a register of type S16. + + Parameters + ---------- + address : int + the register to be read + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message that will contain the value read from the register + """ + return self.send( + HarpMessage.create( + message_type=MessageType.READ, + address=address, + payload_type=PayloadType.S16, + ).frame + ) + + def read_u32(self, address: int) -> ReplyHarpMessage: + """ + Reads the value of a register of type U32. + + Parameters + ---------- + address : int + the register to be read + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message that will contain the value read from the register + """ + return self.send( + HarpMessage.create( + message_type=MessageType.READ, + address=address, + payload_type=PayloadType.U32, + ).frame + ) + + def read_s32(self, address: int) -> ReplyHarpMessage: + """ + Reads the value of a register of type S32. + + Parameters + ---------- + address : int + the register to be read + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message that will contain the value read from the register + """ + return self.send( + HarpMessage.create( + message_type=MessageType.READ, + address=address, + payload_type=PayloadType.S32, + ).frame + ) + + def read_u64(self, address: int) -> ReplyHarpMessage: + """ + Reads the value of a register of type U64. + + Parameters + ---------- + address : int + the register to be read + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message that will contain the value read from the register + """ + return self.send( + HarpMessage.create( + message_type=MessageType.READ, + address=address, + payload_type=PayloadType.U64, + ).frame + ) + + def read_s64(self, address: int) -> ReplyHarpMessage: + """ + Reads the value of a register of type S64. + + Parameters + ---------- + address : int + the register to be read + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message that will contain the value read from the register + """ + return self.send( + HarpMessage.create( + message_type=MessageType.READ, + address=address, + payload_type=PayloadType.S64, + ).frame + ) + + def read_float(self, address: int) -> ReplyHarpMessage: + """ + Reads the value of a register of type Float. + + Parameters + ---------- + address : int + the register to be read + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message that will contain the value read from the register + """ + return self.send( + HarpMessage.create( + message_type=MessageType.READ, + address=address, + payload_type=PayloadType.Float, + ).frame + ) + + def write_u8(self, address: int, value: int | list[int]) -> ReplyHarpMessage: + """ + Writes the value of a register of type U8. + + Parameters + ---------- + address : int + the register to be written on + value: int | list[int] + the value to be written to the register + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message + """ + return self.send( + HarpMessage.create( + message_type=MessageType.WRITE, + address=address, + payload_type=PayloadType.U8, + value=value, + ).frame + ) + + def write_s8(self, address: int, value: int | list[int]) -> ReplyHarpMessage: + """ + Writes the value of a register of type S8. + + Parameters + ---------- + address : int + the register to be written on + value: int | list[int] + the value to be written to the register + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message + """ + return self.send( + HarpMessage.create( + message_type=MessageType.WRITE, + address=address, + payload_type=PayloadType.S8, + value=value, + ).frame + ) + + def write_u16(self, address: int, value: int | list[int]) -> ReplyHarpMessage: + """ + Writes the value of a register of type U16. + + Parameters + ---------- + address : int + the register to be written on + value: int | list[int] + the value to be written to the register + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message + """ + return self.send( + HarpMessage.create( + message_type=MessageType.WRITE, + address=address, + payload_type=PayloadType.U16, + value=value, + ).frame + ) + + def write_s16(self, address: int, value: int | list[int]) -> ReplyHarpMessage: + """ + Writes the value of a register of type S16. + + Parameters + ---------- + address : int + the register to be written on + value: int | list[int] + the value to be written to the register + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message + """ + return self.send( + HarpMessage.create( + message_type=MessageType.WRITE, + address=address, + payload_type=PayloadType.S16, + value=value, + ).frame + ) + + def write_u32(self, address: int, value: int | list[int]) -> ReplyHarpMessage: + """ + Writes the value of a register of type U32. + + Parameters + ---------- + address : int + the register to be written on + value: int | list[int] + the value to be written to the register + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message + """ + return self.send( + HarpMessage.create( + message_type=MessageType.WRITE, + address=address, + payload_type=PayloadType.U32, + value=value, + ).frame + ) + + def write_s32(self, address: int, value: int | list[int]) -> ReplyHarpMessage: + """ + Writes the value of a register of type S32. + + Parameters + ---------- + address : int + the register to be written on + value: int | list[int] + the value to be written to the register + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message + """ + return self.send( + HarpMessage.create( + message_type=MessageType.WRITE, + address=address, + payload_type=PayloadType.S32, + value=value, + ).frame + ) + + def write_u64(self, address: int, value: int | list[int]) -> ReplyHarpMessage: + """ + Writes the value of a register of type U64. + + Parameters + ---------- + address : int + the register to be written on + value: int | list[int] + the value to be written to the register + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message + """ + return self.send( + HarpMessage.create( + message_type=MessageType.WRITE, + address=address, + payload_type=PayloadType.U64, + value=value, + ).frame + ) + + def write_s64(self, address: int, value: int | list[int]) -> ReplyHarpMessage: + """ + Writes the value of a register of type S64. + + Parameters + ---------- + address : int + the register to be written on + value: int | list[int] + the value to be written to the register + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message + """ + return self.send( + HarpMessage.create( + message_type=MessageType.WRITE, + address=address, + payload_type=PayloadType.S64, + value=value, + ).frame + ) + + def write_float(self, address: int, value: float | list[float]) -> ReplyHarpMessage: + """ + Writes the value of a register of type Float. + + Parameters + ---------- + address : int + the register to be written on + value: int | list[int] + the value to be written to the register + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message + """ + return self.send( + HarpMessage.create( + message_type=MessageType.WRITE, + address=address, + payload_type=PayloadType.Float, + value=value, + ).frame + ) + def _read_who_am_i(self) -> int: """ Reads the value stored in the `WHO_AM_I` register. From 3f7922f271b7d50435b7595801ec37e266b81c52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 23 Apr 2025 10:54:58 +0100 Subject: [PATCH 096/267] Remove dump from send method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- pyharp/device.py | 39 ++++++++++++--------------------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/pyharp/device.py b/pyharp/device.py index 355ab6d..2a73eec 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -387,9 +387,7 @@ def reset_device(self) -> ReplyHarpMessage: return reply - def send( - self, message_bytes: bytearray, dump: bool = True - ) -> Optional[ReplyHarpMessage]: + def send(self, message_bytes: bytearray) -> Optional[ReplyHarpMessage]: """ Sends a Harp message. @@ -397,8 +395,6 @@ def send( ---------- message_bytes : bytearray the bytearray containing the message to be sent to the device - dump : bool, optional - indicates whether the reply message should be dumped or not Returns ------- @@ -409,8 +405,7 @@ def send( reply = self._read() - if dump: - self._dump_reply(reply.frame) + self._dump_reply(reply.frame) return reply @@ -898,8 +893,7 @@ def _read_who_am_i(self) -> int: address = CommonRegisters.WHO_AM_I reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U16).frame, - dump=False, + HarpMessage.create(MessageType.READ, address, PayloadType.U16).frame ) return reply.payload_as_int() @@ -927,8 +921,7 @@ def _read_hw_version_h(self) -> int: address = CommonRegisters.HW_VERSION_H reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame, - dump=False, + HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame ) return reply.payload_as_int() @@ -945,8 +938,7 @@ def _read_hw_version_l(self) -> int: address = CommonRegisters.HW_VERSION_L reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame, - dump=False, + HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame ) return reply.payload_as_int() @@ -963,8 +955,7 @@ def _read_assembly_version(self) -> int: address = CommonRegisters.ASSEMBLY_VERSION reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame, - dump=False, + HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame ) return reply.payload_as_int() @@ -981,8 +972,7 @@ def _read_harp_version_h(self) -> int: address = CommonRegisters.HARP_VERSION_H reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame, - dump=False, + HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame ) return reply.payload_as_int() @@ -999,8 +989,7 @@ def _read_harp_version_l(self) -> int: address = CommonRegisters.HARP_VERSION_L reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame, - dump=False, + HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame ) return reply.payload_as_int() @@ -1017,8 +1006,7 @@ def _read_fw_version_h(self) -> int: address = CommonRegisters.FIRMWARE_VERSION_H reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame, - dump=False, + HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame ) return reply.payload_as_int() @@ -1035,8 +1023,7 @@ def _read_fw_version_l(self) -> int: address = CommonRegisters.FIRMWARE_VERSION_L reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame, - dump=False, + HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame ) return reply.payload_as_int() @@ -1053,8 +1040,7 @@ def _read_device_name(self) -> str: address = CommonRegisters.DEVICE_NAME reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame, - dump=False, + HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame ) return reply.payload_as_string() @@ -1071,8 +1057,7 @@ def _read_serial_number(self) -> int: address = CommonRegisters.SERIAL_NUMBER reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame, - dump=False, + HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame ) if reply.is_error(): From 924997be28b3162e8a57a340221bf9a26640a652 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 23 Apr 2025 11:38:46 +0100 Subject: [PATCH 097/267] Add ClockConfig and TimestampOffset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- pyharp/base.py | 41 ++++++++++++++++++++++- pyharp/device.py | 85 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 1 deletion(-) diff --git a/pyharp/base.py b/pyharp/base.py index c3a0750..2c2d393 100644 --- a/pyharp/base.py +++ b/pyharp/base.py @@ -1,4 +1,4 @@ -from enum import IntEnum +from enum import IntEnum, IntFlag # Bit masks for the PayloadType _isUnsigned: int = 0x00 @@ -133,6 +133,10 @@ class CommonRegisters(IntEnum): the number of the `DEVICE_NAME` register SERIAL_NUMBER : int the number of the `SERIAL_NUMBER` register + R_CLOCK_CONFIG : int + the number of the `R_CLOCK_CONFIG` register + R_TIMESTAMP_OFFSET : int + the number of the `R_TIMESTAMP_OFFSET` register """ WHO_AM_I = 0x00 @@ -149,6 +153,8 @@ class CommonRegisters(IntEnum): RESET_DEV = 0x0B DEVICE_NAME = 0x0C SERIAL_NUMBER = 0x0D + R_CLOCK_CONFIG = 0x0E + R_TIMESTAMP_OFFSET = 0x0F class OperationMode(IntEnum): @@ -171,3 +177,36 @@ class OperationMode(IntEnum): ACTIVE = 1 RESERVED = 2 SPEED = 3 + + +class ClockConfig(IntFlag): + """ + An enumeration with the clock configuration bits for the R_CLOCK_CONFIG register of a Harp device. + More information can be found [here](https://harp-tech.org/protocol/Device.html#r_clock_config-u8--synchronization-clock-configuration). + + Attributes + ---------- + CLK_REP : int + Bit 0 (0x01): If set to 1, the device will repeat the Harp Synchronization Clock to the Clock Output connector, if available. + Acts as a daisy-chain by repeating the Clock Input to the Clock Output. Setting this bit also unlocks the Harp Synchronization Clock. + CLK_GEN : int + Bit 1 (0x02): If set to 1, the device will generate Harp Synchronization Clock to the Clock Output connector, if available. + The Clock Input will be ignored. Read as 1 if the device is generating the Harp Synchronization Clock. + REP_ABLE : int + Bit 3 (0x08, read-only): Indicates if the device is able (1) to repeat the Harp Synchronization Clock timestamp. + GEN_ABLE : int + Bit 4 (0x10, read-only): Indicates if the device is able (1) to generate the Harp Synchronization Clock timestamp. + CLK_UNLOCK : int + Bit 6 (0x40): If set to 1, the device will unlock the timestamp register counter (R_TIMESTAMP_SECOND) and accept new timestamp values. + Read as 1 if the timestamp register is unlocked. + CLK_LOCK : int + Bit 7 (0x80): If set to 1, the device will lock the current timestamp register counter (R_TIMESTAMP_SECOND) and reject new timestamp values. + Read as 1 if the timestamp register is locked. + """ + + CLK_REP = 0x01 + CLK_GEN = 0x02 + REP_ABLE = 0x08 + GEN_ABLE = 0x10 + CLK_UNLOCK = 0x40 + CLK_LOCK = 0x80 \ No newline at end of file diff --git a/pyharp/device.py b/pyharp/device.py index 2a73eec..feb9490 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -9,6 +9,7 @@ import serial from pyharp import CommonRegisters, MessageType, OperationMode, PayloadType +from pyharp.base import ClockConfig from pyharp.device_names import device_names from pyharp.harp_serial import HarpSerial from pyharp.messages import HarpMessage, ReplyHarpMessage @@ -55,6 +56,8 @@ class Device: FIRMWARE_VERSION_L: int DEVICE_NAME: str SERIAL_NUMBER: int + CLOCK_CONFIG: int + TIMESTAMP_OFFSET: int _ser: HarpSerial _dump_file_path: Path @@ -105,6 +108,8 @@ def load(self) -> None: self.FIRMWARE_VERSION_L = self._read_fw_version_l() self.DEVICE_NAME = self._read_device_name() self.SERIAL_NUMBER = self._read_serial_number() + self.CLOCK_CONFIG = self._read_clock_config() + self.TIMESTAMP_OFFSET = self._read_timestamp_offset() def info(self) -> None: """ @@ -387,6 +392,52 @@ def reset_device(self) -> ReplyHarpMessage: return reply + def set_clock_config(self, clock_config: ClockConfig) -> ReplyHarpMessage: + """ + Sets the clock configuration of the device. + + Parameters + ---------- + clock_config : ClockConfig + the clock configuration value + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message + """ + address = CommonRegisters.CLOCK_CONFIG + reply = self.send( + HarpMessage.create( + MessageType.WRITE, address, PayloadType.U8, clock_config + ).frame + ) + + return reply + + def set_timestamp_offset(self, timestamp_offset: int) -> ReplyHarpMessage: + """ + When the value of this register is above 0 (zero), the device's timestamp will be offset by this amount. The register is sensitive to 500 microsecond increments. This register is non-volatile. + + Parameters + ---------- + timestamp_offset : int + the timestamp offset value + + Returns + ------- + ReplyHarpMessage + the reply to the Harp message + """ + address = CommonRegisters.TIMESTAMP_OFFSET + reply = self.send( + HarpMessage.create( + MessageType.WRITE, address, PayloadType.U8, timestamp_offset + ).frame + ) + + return reply + def send(self, message_bytes: bytearray) -> Optional[ReplyHarpMessage]: """ Sends a Harp message. @@ -1065,6 +1116,40 @@ def _read_serial_number(self) -> int: return reply.payload_as_int() + def _read_clock_config(self) -> int: + """ + Reads the value stored in the `CLOCK_CONFIG` register. + + Returns + ------- + int + the value of the `CLOCK_CONFIG` register. + """ + address = CommonRegisters.CLOCK_CONFIG + + reply: ReplyHarpMessage = self.send( + HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame + ) + + return reply.payload_as_int() + + def _read_timestamp_offset(self) -> int: + """ + Reads the value stored in the `TIMESTAMP_OFFSET` register. + + Returns + ------- + int + the value of the `TIMESTAMP_OFFSET` register. + """ + address = CommonRegisters.TIMESTAMP_OFFSET + + reply: ReplyHarpMessage = self.send( + HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame + ) + + return reply.payload_as_int() + def __enter__(self): """ Support for using Device with 'with' statement. From 3c21927802d064e12f01c3d4e68096c6317baacc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 23 Apr 2025 11:39:56 +0100 Subject: [PATCH 098/267] Add OperationCtrl and ResetMode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- pyharp/base.py | 63 ++++++++++++++++++++++++++++++++++++++++++++++++ pyharp/device.py | 31 ++++++++++++------------ 2 files changed, 79 insertions(+), 15 deletions(-) diff --git a/pyharp/base.py b/pyharp/base.py index 2c2d393..7867df9 100644 --- a/pyharp/base.py +++ b/pyharp/base.py @@ -178,6 +178,69 @@ class OperationMode(IntEnum): RESERVED = 2 SPEED = 3 +class OperationCtrl(IntFlag): + """ + An enumeration with the operation control bits of a Harp device. More information on the operation control bits can be found [here](https://harp-tech.org/protocol/Device.html#r_operation_ctrl-u16--operation-mode-configuration). + + Attributes + ---------- + OP_MODE : int + Bits 1:0 (0x03): Operation mode of the device. + 0: Standby Mode (all Events off, mandatory) + 1: Active Mode (Events detection enabled, mandatory) + 2: Reserved + 3: Speed Mode (device enters Speed Mode, optional; only responds to Speed Mode commands) + DUMP : int + Bit 3 (0x08): When set to 1, the device adds the content of all registers to the streaming buffer as Read messages. Always read as 0. + MUTE_RPL : int + Bit 4 (0x10): If set to 1, replies to all commands are muted (not sent by the device). + VISUALEN : int + Bit 5 (0x20): If set to 1, visual indications (e.g., LEDs) operate. If 0, all visual indications are turned off. + OPLEDEN : int + Bit 6 (0x40): If set to 1, the LED indicates the selected Operation Mode (see LED feedback table in documentation). + ALIVE_EN : int + Bit 7 (0x80): If set to 1, the device sends an Event Message with the R_TIMESTAMP_SECONDS content each second (heartbeat). + """ + + OP_MODE = 3 << 0 + DUMP = 1 << 3 + MUTE_RPL = 1 << 4 + VISUALEN = 1 << 5 + OPLEDEN = 1 << 6 + ALIVE_EN = 1 << 7 + + +class ResetMode(IntEnum): + """ + An enumeration with the reset modes and actions for the R_RESET_DEV register of a Harp device. + More information on the reset modes can be found [here](https://harp-tech.org/protocol/Device.html#r_reset_dev-u8--reset-device-and-save-non-volatile-registers). + + Attributes + ---------- + RST_DEF : int + Bit 0 (0x01): If set, resets the device and restores all registers (Common and Application) to default values. + EEPROM is erased and defaults become the permanent boot option. + RST_EE : int + Bit 1 (0x02): If set, resets the device and restores all registers (Common and Application) from non-volatile memory (EEPROM). + EEPROM values remain the permanent boot option. + SAVE : int + Bit 3 (0x08): If set, saves all non-volatile registers (Common and Application) to EEPROM and reboots. + EEPROM becomes the permanent boot option. + NAME_TO_DEFAULT : int + Bit 4 (0x10): If set, reboots the device with the default name. + BOOT_DEF : int + Bit 6 (0x40, read-only): Indicates the device booted with default register values. + BOOT_EE : int + Bit 7 (0x80, read-only): Indicates the device booted with register values saved on the EEPROM. + """ + + RST_DEF = 0x01 + RST_EE = 0x02 + SAVE = 0x08 + NAME_TO_DEFAULT = 0x10 + BOOT_DEF = 0x40 + BOOT_EE = 0x80 + class ClockConfig(IntFlag): """ diff --git a/pyharp/device.py b/pyharp/device.py index feb9490..aa096fb 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -9,7 +9,7 @@ import serial from pyharp import CommonRegisters, MessageType, OperationMode, PayloadType -from pyharp.base import ClockConfig +from pyharp.base import ClockConfig, OperationCtrl, ResetMode from pyharp.device_names import device_names from pyharp.harp_serial import HarpSerial from pyharp.messages import HarpMessage, ReplyHarpMessage @@ -169,7 +169,7 @@ def _read_device_mode(self) -> OperationMode: reply = self.send( HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame ) - return OperationMode(reply.payload_as_int() & 0x03) + return OperationMode(reply.payload_as_int() & OperationCtrl.OP_MODE) def dump_registers(self) -> list: """ @@ -186,7 +186,7 @@ def dump_registers(self) -> list: HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame ).payload_as_int() # Assert DUMP bit - reg_value |= 0x08 + reg_value |= OperationCtrl.DUMP self.send( HarpMessage.create( MessageType.WRITE, address, PayloadType.U8, reg_value @@ -225,7 +225,7 @@ def set_mode(self, mode: OperationMode) -> ReplyHarpMessage: ).payload_as_int() # Clear old operation mode - reg_value &= ~0x03 + reg_value &= ~OperationCtrl.OP_MODE # Set new operation mode reg_value |= mode @@ -259,9 +259,9 @@ def alive_en(self, enable: bool) -> bool: ).payload_as_int() if enable: - reg_value |= 1 << 7 + reg_value |= OperationCtrl.ALIVE_EN else: - reg_value &= ~(1 << 7) + reg_value &= ~OperationCtrl.ALIVE_EN reply = self.send( HarpMessage.create( @@ -293,9 +293,9 @@ def op_led_en(self, enable: bool) -> bool: ).payload_as_int() if enable: - reg_value |= 1 << 6 + reg_value |= OperationCtrl.OPLEDEN else: - reg_value &= ~(1 << 6) + reg_value &= ~OperationCtrl.OPLEDEN reply = self.send( HarpMessage.create( @@ -327,9 +327,9 @@ def status_led(self, enable: bool) -> bool: ).payload_as_int() if enable: - reg_value |= 1 << 5 + reg_value |= OperationCtrl.STATUS_LED else: - reg_value &= ~(1 << 5) + reg_value &= ~OperationCtrl.STATUS_LED reply = self.send( HarpMessage.create( @@ -361,9 +361,9 @@ def mute_reply(self, enable: bool) -> bool: ).payload_as_int() if enable: - reg_value |= 1 << 4 + reg_value |= OperationCtrl.MUTE_RPL else: - reg_value &= ~(1 << 4) + reg_value &= ~OperationCtrl.MUTE_RPL reply = self.send( HarpMessage.create( @@ -373,7 +373,9 @@ def mute_reply(self, enable: bool) -> bool: return reply - def reset_device(self) -> ReplyHarpMessage: + def reset_device( + self, reset_mode: ResetMode = ResetMode.RST_DEF + ) -> ReplyHarpMessage: """ Resets the device and reboots with all the registers with the default values. Beware that the EEPROM will be erased. More information on the reset device register can be found [here](https://harp-tech.org/protocol/Device.html#r_reset_dev-u8--reset-device-and-save-non-volatile-registers). @@ -383,10 +385,9 @@ def reset_device(self) -> ReplyHarpMessage: the reply to the Harp message """ address = CommonRegisters.RESET_DEV - reset_value = 0x01 reply = self.send( HarpMessage.create( - MessageType.WRITE, address, PayloadType.U8, reset_value + MessageType.WRITE, address, PayloadType.U8, reset_mode ).frame ) From ec85295a2ac6e7c084ec5ff95e5bfed0659255d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 23 Apr 2025 11:40:29 +0100 Subject: [PATCH 099/267] Allow Port control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- pyharp/messages.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/pyharp/messages.py b/pyharp/messages.py index cc03b85..19559a6 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -34,6 +34,7 @@ class HarpMessage: def __init__(self): self._frame = bytearray() + self._port = self.DEFAULT_PORT def calculate_checksum(self) -> int: """ @@ -109,6 +110,18 @@ def port(self) -> int: """ return self._frame[3] + @port.setter + def port(self, value: int) -> None: + """ + Sets the port value. + + Parameters + ---------- + value : int + the port value to set + """ + self._port = value + @property def payload_type(self) -> PayloadType: """ @@ -471,7 +484,7 @@ def __init__(self, payload_type: PayloadType, address: int): length: int = 4 self._frame.append(length) self._frame.append(address) - self._frame.append(self.DEFAULT_PORT) + self._frame.append(self._port) self._frame.append(payload_type) self._frame.append(self.calculate_checksum()) @@ -548,7 +561,7 @@ def __init__( # Length is BASE_LENGTH + payload size self._frame.append(self.BASE_LENGTH + len(payload)) self._frame.append(address) - self._frame.append(self.DEFAULT_PORT) + self._frame.append(self._port) self._frame.append(payload_type) self._frame += payload self._frame.append(self.calculate_checksum()) From be6f11c60c2239b0ee059cb959fe104557bf0604 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 23 Apr 2025 16:45:16 +0100 Subject: [PATCH 100/267] Fix issue with payload property MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- pyharp/messages.py | 82 +++++++++++++++++++++++++++++++--------------- 1 file changed, 56 insertions(+), 26 deletions(-) diff --git a/pyharp/messages.py b/pyharp/messages.py index 19559a6..76b1ef8 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -148,120 +148,150 @@ def payload(self) -> Union[int, list[int]]: if self.payload_type & PayloadType.Timestamp: payload_start += 6 + payload_index = payload_start + 1 + # length is payload_start + payload type size match self.payload_type: case PayloadType.U8 | PayloadType.TimestampedU8: if self.length == payload_start + 1: - return self._frame[5] + return self._frame[payload_index] else: # array case return [ int.from_bytes([self._frame[i]], byteorder="little") - for i in range(5, self.length + 1) + for i in range(payload_index, self.length + 1) ] case PayloadType.S8 | PayloadType.TimestampedS8: if self.length == payload_start + 1: return int.from_bytes( - [self._frame[5]], byteorder="little", signed=True + [self._frame[payload_index]], byteorder="little", signed=True ) else: # array case return [ int.from_bytes( - [self._frame[i]], byteorder="little", signed=True + [self._frame[i]], + byteorder="little", + signed=True, ) - for i in range(5, self.length + 1) + for i in range(payload_index, self.length + 1) ] case PayloadType.U16 | PayloadType.TimestampedU16: if self.length == payload_start + 2: return int.from_bytes( - self._frame[5:7], byteorder="little", signed=False + self._frame[payload_index : payload_index + 2], + byteorder="little", + signed=False, ) else: # array case return [ int.from_bytes( - self._frame[i : i + 2], byteorder="little", signed=False + self._frame[i : i + 2], + byteorder="little", + signed=False, ) - for i in range(5, self.length + 1, 2) + for i in range(payload_index, self.length + 1, 2) ] case PayloadType.S16 | PayloadType.TimestampedS16: if self.length == payload_start + 2: return int.from_bytes( - self._frame[5:7], byteorder="little", signed=True + self._frame[payload_index : payload_index + 2], + byteorder="little", + signed=True, ) else: return [ int.from_bytes( - self._frame[i : i + 2], byteorder="little", signed=True + self._frame[i : i + 2], + byteorder="little", + signed=True, ) - for i in range(5, self.length + 1, 2) + for i in range(payload_index, self.length + 1, 2) ] case PayloadType.U32 | PayloadType.TimestampedU32: if self.length == payload_start + 4: return int.from_bytes( - self._frame[5:9], byteorder="little", signed=False + self._frame[payload_index : payload_index + 4], + byteorder="little", + signed=False, ) else: return [ int.from_bytes( - self._frame[i : i + 4], byteorder="little", signed=False + self._frame[i : i + 4], + byteorder="little", + signed=False, ) - for i in range(5, self.length + 1, 4) + for i in range(payload_index, self.length + 1, 4) ] case PayloadType.S32 | PayloadType.TimestampedS32: if self.length == payload_start + 4: return int.from_bytes( - self._frame[5:9], byteorder="little", signed=True + self._frame[payload_index : payload_index + 4], + byteorder="little", + signed=True, ) else: return [ int.from_bytes( - self._frame[i : i + 4], byteorder="little", signed=True + self._frame[i : i + 4], + byteorder="little", + signed=True, ) - for i in range(5, self.length + 1, 4) + for i in range(payload_index, self.length + 1, 4) ] case PayloadType.U64 | PayloadType.TimestampedU64: if self.length == payload_start + 8: return int.from_bytes( - self._frame[5:13], byteorder="little", signed=False + self._frame[payload_index : payload_index + 8], + byteorder="little", + signed=False, ) else: return [ int.from_bytes( - self._frame[i : i + 8], byteorder="little", signed=False + self._frame[i : i + 8], + byteorder="little", + signed=False, ) - for i in range(5, self.length + 1, 8) + for i in range(payload_index, self.length + 1, 8) ] case PayloadType.S64 | PayloadType.TimestampedS64: if self.length == payload_start + 8: return int.from_bytes( - self._frame[5:13], byteorder="little", signed=True + self._frame[payload_index : payload_index + 8], + byteorder="little", + signed=True, ) else: return [ int.from_bytes( - self._frame[i : i + 8], byteorder="little", signed=True + self._frame[i : i + 8], + byteorder="little", + signed=True, ) - for i in range(5, self.length + 1, 8) + for i in range(payload_index, self.length + 1, 8) ] case PayloadType.Float | PayloadType.TimestampedFloat: if self.length == payload_start + 4: - return struct.unpack(" int: From de5a799f90d6727ab83ae3af52e3a9f50ca9944d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 23 Apr 2025 16:46:34 +0100 Subject: [PATCH 101/267] Update payload_as_string MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- pyharp/messages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyharp/messages.py b/pyharp/messages.py index 76b1ef8..023ff41 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -484,7 +484,7 @@ def payload_as_string(self) -> str: str the payload parsed as a str """ - return self._raw_payload.decode("utf-8") + return self._raw_payload.decode("utf-8").rstrip("\x00") # TODO: handle float case and/or delete functional altogether def payload_as_float(self) -> float: From 542de32de7fbc60c6655a06c9a3c84a131a360eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 23 Apr 2025 16:50:01 +0100 Subject: [PATCH 102/267] Update CommonRegisters members name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- pyharp/base.py | 12 ++++++------ tests/test_device.py | 2 +- tests/test_messages.py | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pyharp/base.py b/pyharp/base.py index 7867df9..0d5d3f0 100644 --- a/pyharp/base.py +++ b/pyharp/base.py @@ -133,10 +133,10 @@ class CommonRegisters(IntEnum): the number of the `DEVICE_NAME` register SERIAL_NUMBER : int the number of the `SERIAL_NUMBER` register - R_CLOCK_CONFIG : int - the number of the `R_CLOCK_CONFIG` register - R_TIMESTAMP_OFFSET : int - the number of the `R_TIMESTAMP_OFFSET` register + CLOCK_CONFIG : int + the number of the `CLOCK_CONFIG` register + TIMESTAMP_OFFSET : int + the number of the `TIMESTAMP_OFFSET` register """ WHO_AM_I = 0x00 @@ -153,8 +153,8 @@ class CommonRegisters(IntEnum): RESET_DEV = 0x0B DEVICE_NAME = 0x0C SERIAL_NUMBER = 0x0D - R_CLOCK_CONFIG = 0x0E - R_TIMESTAMP_OFFSET = 0x0F + CLOCK_CONFIG = 0x0E + TIMESTAMP_OFFSET = 0x0F class OperationMode(IntEnum): diff --git a/tests/test_device.py b/tests/test_device.py index fb85b39..d511549 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -57,7 +57,7 @@ # # read register 38 # reply = device.read_u8(register) # assert reply is not None -# assert reply.payload_as_int() == write_value +# assert reply.payload == write_value # device.disconnect() diff --git a/tests/test_messages.py b/tests/test_messages.py index ed0679d..cf5ba1d 100644 --- a/tests/test_messages.py +++ b/tests/test_messages.py @@ -452,10 +452,10 @@ def test_harp_message_parse() -> None: frame = bytearray( [ MessageType.READ, - 5, + 11, 42, 255, - PayloadType.U8, + PayloadType.TimestampedU8, 0, 0, 0, @@ -475,7 +475,7 @@ def test_harp_message_parse() -> None: assert isinstance(message, ReplyHarpMessage) assert message.message_type == MessageType.READ assert message.address == 42 - assert message.payload_as_int() == 123 + assert message.payload == 123 def test_timestamp_handling() -> None: From 57966217b300bbd1334b2f03f164f28c10e42156 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 23 Apr 2025 16:55:06 +0100 Subject: [PATCH 103/267] Update send method to receive HarpMessage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- README.md | 8 +- .../olfactometer_example.py | 56 ++++---- docs/index.md | 8 +- pyharp/device.py | 134 ++++++++---------- pyharp/drivers/behavior.py | 28 ++-- tests/test_device.py | 4 +- 6 files changed, 104 insertions(+), 134 deletions(-) diff --git a/README.md b/README.md index d1485ef..79b36f3 100644 --- a/README.md +++ b/README.md @@ -27,10 +27,10 @@ device.info() register_address = 32 # Read from register -value = device.send(HarpMessage.create(MessageType.READ, register_address, PayloadType.U8).frame) +value = device.send(HarpMessage.create(MessageType.READ, register_address, PayloadType.U8)) # Write to register -device.send(HarpMessage.create(MessageType.WRITE, register_address, PayloadType.U8, value).frame) +device.send(HarpMessage.create(MessageType.WRITE, register_address, PayloadType.U8, value)) # Disconnect when done device.disconnect() @@ -51,10 +51,10 @@ with Device("/dev/ttyUSB0") as device: register_address = 32 # Read from register - value = device.send(HarpMessage.create(MessageType.READ, register_address, PayloadType.U8).frame) + value = device.send(HarpMessage.create(MessageType.READ, register_address, PayloadType.U8)) # Write to register - device.send(HarpMessage.create(MessageType.WRITE, register_address, PayloadType.U8, value).frame) + device.send(HarpMessage.create(MessageType.WRITE, register_address, PayloadType.U8, value)) ``` ## for Linux diff --git a/docs/examples/olfactometer_example/olfactometer_example.py b/docs/examples/olfactometer_example/olfactometer_example.py index aea85b1..c4b2fe3 100644 --- a/docs/examples/olfactometer_example/olfactometer_example.py +++ b/docs/examples/olfactometer_example/olfactometer_example.py @@ -42,7 +42,7 @@ def main(): device.set_mode(OperationMode.ACTIVE) # Enable flow - device.send(HarpMessage.WriteU8(32, 0x01).frame) + device.send(HarpMessage.create(MessageType.WRITE, 32, PayloadType.U8, 0x01)) # Initialize thread for events events_thread = Thread( @@ -58,59 +58,51 @@ def main(): device.send( HarpMessage.create( MessageType.WRITE, 42, PayloadType.Float, int(random.random() * 100) - ).frame + ) ) device.send( HarpMessage.create( MessageType.WRITE, 43, PayloadType.Float, int(random.random() * 100) - ).frame + ) ) device.send( HarpMessage.create( MessageType.WRITE, 44, PayloadType.Float, int(random.random() * 100) - ).frame + ) ) device.send( HarpMessage.create( MessageType.WRITE, 45, PayloadType.Float, int(random.random() * 100) - ).frame + ) ) # Open every odor valve, one at a time every 5 seconds - device.send( - HarpMessage.create(MessageType.WRITE, 68, PayloadType.Float, 0x01).frame - ) + device.send(HarpMessage.create(MessageType.WRITE, 68, PayloadType.Float, 0x01)) + time.sleep(5) - device.send( - HarpMessage.create(MessageType.WRITE, 69, PayloadType.Float, 0x01).frame - ) - device.send( - HarpMessage.create(MessageType.WRITE, 68, PayloadType.Float, 0x02).frame - ) + + device.send(HarpMessage.create(MessageType.WRITE, 69, PayloadType.Float, 0x01)) + device.send(HarpMessage.create(MessageType.WRITE, 68, PayloadType.Float, 0x02)) + time.sleep(5) - device.send( - HarpMessage.create(MessageType.WRITE, 69, PayloadType.Float, 0x02).frame - ) - device.send( - HarpMessage.create(MessageType.WRITE, 68, PayloadType.Float, 0x04).frame - ) + + device.send(HarpMessage.create(MessageType.WRITE, 69, PayloadType.Float, 0x02)) + device.send(HarpMessage.create(MessageType.WRITE, 68, PayloadType.Float, 0x04)) + time.sleep(5) - device.send( - HarpMessage.create(MessageType.WRITE, 69, PayloadType.Float, 0x04).frame - ) - device.send( - HarpMessage.create(MessageType.WRITE, 68, PayloadType.Float, 0x08).frame - ) + + device.send(HarpMessage.create(MessageType.WRITE, 69, PayloadType.Float, 0x04)) + device.send(HarpMessage.create(MessageType.WRITE, 68, PayloadType.Float, 0x08)) + time.sleep(5) - device.send( - HarpMessage.create(MessageType.WRITE, 69, PayloadType.Float, 0x08).frame - ) + + device.send(HarpMessage.create(MessageType.WRITE, 69, PayloadType.Float, 0x08)) + time.sleep(5) # Disable flow - device.send( - HarpMessage.create(MessageType.WRITE, 32, PayloadType.Float, 0x00).frame - ) + device.send(HarpMessage.create(MessageType.WRITE, 32, PayloadType.Float, 0x00)) + time.sleep(1) stop_flag.set() diff --git a/docs/index.md b/docs/index.md index d1485ef..79b36f3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -27,10 +27,10 @@ device.info() register_address = 32 # Read from register -value = device.send(HarpMessage.create(MessageType.READ, register_address, PayloadType.U8).frame) +value = device.send(HarpMessage.create(MessageType.READ, register_address, PayloadType.U8)) # Write to register -device.send(HarpMessage.create(MessageType.WRITE, register_address, PayloadType.U8, value).frame) +device.send(HarpMessage.create(MessageType.WRITE, register_address, PayloadType.U8, value)) # Disconnect when done device.disconnect() @@ -51,10 +51,10 @@ with Device("/dev/ttyUSB0") as device: register_address = 32 # Read from register - value = device.send(HarpMessage.create(MessageType.READ, register_address, PayloadType.U8).frame) + value = device.send(HarpMessage.create(MessageType.READ, register_address, PayloadType.U8)) # Write to register - device.send(HarpMessage.create(MessageType.WRITE, register_address, PayloadType.U8, value).frame) + device.send(HarpMessage.create(MessageType.WRITE, register_address, PayloadType.U8, value)) ``` ## for Linux diff --git a/pyharp/device.py b/pyharp/device.py index aa096fb..a6d10f2 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -166,10 +166,8 @@ def _read_device_mode(self) -> OperationMode: the current device mode """ address = CommonRegisters.OPERATION_CTRL - reply = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame - ) - return OperationMode(reply.payload_as_int() & OperationCtrl.OP_MODE) + reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8)) + return OperationMode(reply.payload & OperationCtrl.OP_MODE) def dump_registers(self) -> list: """ @@ -183,14 +181,12 @@ def dump_registers(self) -> list: """ address = CommonRegisters.OPERATION_CTRL reg_value = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame - ).payload_as_int() + HarpMessage.create(MessageType.READ, address, PayloadType.U8) + ).payload # Assert DUMP bit reg_value |= OperationCtrl.DUMP self.send( - HarpMessage.create( - MessageType.WRITE, address, PayloadType.U8, reg_value - ).frame + HarpMessage.create(MessageType.WRITE, address, PayloadType.U8, reg_value) ) # Receive the contents of all registers as Harp Read Reply Messages @@ -221,8 +217,8 @@ def set_mode(self, mode: OperationMode) -> ReplyHarpMessage: # Read register first reg_value = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame - ).payload_as_int() + HarpMessage.create(MessageType.READ, address, PayloadType.U8) + ).payload # Clear old operation mode reg_value &= ~OperationCtrl.OP_MODE @@ -230,9 +226,7 @@ def set_mode(self, mode: OperationMode) -> ReplyHarpMessage: # Set new operation mode reg_value |= mode reply = self.send( - HarpMessage.create( - MessageType.WRITE, address, PayloadType.U8, reg_value - ).frame + HarpMessage.create(MessageType.WRITE, address, PayloadType.U8, reg_value) ) return reply @@ -255,8 +249,8 @@ def alive_en(self, enable: bool) -> bool: # Read register first reg_value = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame - ).payload_as_int() + HarpMessage.create(MessageType.READ, address, PayloadType.U8) + ).payload if enable: reg_value |= OperationCtrl.ALIVE_EN @@ -264,9 +258,7 @@ def alive_en(self, enable: bool) -> bool: reg_value &= ~OperationCtrl.ALIVE_EN reply = self.send( - HarpMessage.create( - MessageType.WRITE, address, PayloadType.U8, reg_value - ).frame + HarpMessage.create(MessageType.WRITE, address, PayloadType.U8, reg_value) ) return reply @@ -289,8 +281,8 @@ def op_led_en(self, enable: bool) -> bool: # Read register first reg_value = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame - ).payload_as_int() + HarpMessage.create(MessageType.READ, address, PayloadType.U8) + ).payload if enable: reg_value |= OperationCtrl.OPLEDEN @@ -298,9 +290,7 @@ def op_led_en(self, enable: bool) -> bool: reg_value &= ~OperationCtrl.OPLEDEN reply = self.send( - HarpMessage.create( - MessageType.WRITE, address, PayloadType.U8, reg_value - ).frame + HarpMessage.create(MessageType.WRITE, address, PayloadType.U8, reg_value) ) return reply @@ -323,8 +313,8 @@ def status_led(self, enable: bool) -> bool: # Read register first reg_value = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame - ).payload_as_int() + HarpMessage.create(MessageType.READ, address, PayloadType.U8) + ).payload if enable: reg_value |= OperationCtrl.STATUS_LED @@ -332,9 +322,7 @@ def status_led(self, enable: bool) -> bool: reg_value &= ~OperationCtrl.STATUS_LED reply = self.send( - HarpMessage.create( - MessageType.WRITE, address, PayloadType.U8, reg_value - ).frame + HarpMessage.create(MessageType.WRITE, address, PayloadType.U8, reg_value) ) return reply @@ -357,8 +345,8 @@ def mute_reply(self, enable: bool) -> bool: # Read register first reg_value = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame - ).payload_as_int() + HarpMessage.create(MessageType.READ, address, PayloadType.U8) + ).payload if enable: reg_value |= OperationCtrl.MUTE_RPL @@ -366,9 +354,7 @@ def mute_reply(self, enable: bool) -> bool: reg_value &= ~OperationCtrl.MUTE_RPL reply = self.send( - HarpMessage.create( - MessageType.WRITE, address, PayloadType.U8, reg_value - ).frame + HarpMessage.create(MessageType.WRITE, address, PayloadType.U8, reg_value) ) return reply @@ -386,9 +372,7 @@ def reset_device( """ address = CommonRegisters.RESET_DEV reply = self.send( - HarpMessage.create( - MessageType.WRITE, address, PayloadType.U8, reset_mode - ).frame + HarpMessage.create(MessageType.WRITE, address, PayloadType.U8, reset_mode) ) return reply @@ -409,9 +393,7 @@ def set_clock_config(self, clock_config: ClockConfig) -> ReplyHarpMessage: """ address = CommonRegisters.CLOCK_CONFIG reply = self.send( - HarpMessage.create( - MessageType.WRITE, address, PayloadType.U8, clock_config - ).frame + HarpMessage.create(MessageType.WRITE, address, PayloadType.U8, clock_config) ) return reply @@ -434,30 +416,30 @@ def set_timestamp_offset(self, timestamp_offset: int) -> ReplyHarpMessage: reply = self.send( HarpMessage.create( MessageType.WRITE, address, PayloadType.U8, timestamp_offset - ).frame + ) ) return reply - def send(self, message_bytes: bytearray) -> Optional[ReplyHarpMessage]: + def send(self, message: HarpMessage) -> Optional[ReplyHarpMessage]: """ Sends a Harp message. Parameters ---------- - message_bytes : bytearray - the bytearray containing the message to be sent to the device + message_bytes : HarpMessage + the HarpMessage containing the message to be sent to the device Returns ------- Optional[ReplyHarpMessage] the reply to the Harp message or None if no reply is given """ - self._ser.write(message_bytes) + self._ser.write(message) reply = self._read() - self._dump_reply(reply.frame) + self._dump_reply(reply) return reply @@ -529,7 +511,7 @@ def read_u8(self, address: int) -> ReplyHarpMessage: message_type=MessageType.READ, address=address, payload_type=PayloadType.U8, - ).frame + ) ) def read_s8(self, address: int) -> ReplyHarpMessage: @@ -551,7 +533,7 @@ def read_s8(self, address: int) -> ReplyHarpMessage: message_type=MessageType.READ, address=address, payload_type=PayloadType.S8, - ).frame + ) ) def read_u16(self, address: int) -> ReplyHarpMessage: @@ -573,7 +555,7 @@ def read_u16(self, address: int) -> ReplyHarpMessage: message_type=MessageType.READ, address=address, payload_type=PayloadType.U16, - ).frame + ) ) def read_s16(self, address: int) -> ReplyHarpMessage: @@ -595,7 +577,7 @@ def read_s16(self, address: int) -> ReplyHarpMessage: message_type=MessageType.READ, address=address, payload_type=PayloadType.S16, - ).frame + ) ) def read_u32(self, address: int) -> ReplyHarpMessage: @@ -617,7 +599,7 @@ def read_u32(self, address: int) -> ReplyHarpMessage: message_type=MessageType.READ, address=address, payload_type=PayloadType.U32, - ).frame + ) ) def read_s32(self, address: int) -> ReplyHarpMessage: @@ -639,7 +621,7 @@ def read_s32(self, address: int) -> ReplyHarpMessage: message_type=MessageType.READ, address=address, payload_type=PayloadType.S32, - ).frame + ) ) def read_u64(self, address: int) -> ReplyHarpMessage: @@ -661,7 +643,7 @@ def read_u64(self, address: int) -> ReplyHarpMessage: message_type=MessageType.READ, address=address, payload_type=PayloadType.U64, - ).frame + ) ) def read_s64(self, address: int) -> ReplyHarpMessage: @@ -683,7 +665,7 @@ def read_s64(self, address: int) -> ReplyHarpMessage: message_type=MessageType.READ, address=address, payload_type=PayloadType.S64, - ).frame + ) ) def read_float(self, address: int) -> ReplyHarpMessage: @@ -705,7 +687,7 @@ def read_float(self, address: int) -> ReplyHarpMessage: message_type=MessageType.READ, address=address, payload_type=PayloadType.Float, - ).frame + ) ) def write_u8(self, address: int, value: int | list[int]) -> ReplyHarpMessage: @@ -730,7 +712,7 @@ def write_u8(self, address: int, value: int | list[int]) -> ReplyHarpMessage: address=address, payload_type=PayloadType.U8, value=value, - ).frame + ) ) def write_s8(self, address: int, value: int | list[int]) -> ReplyHarpMessage: @@ -755,7 +737,7 @@ def write_s8(self, address: int, value: int | list[int]) -> ReplyHarpMessage: address=address, payload_type=PayloadType.S8, value=value, - ).frame + ) ) def write_u16(self, address: int, value: int | list[int]) -> ReplyHarpMessage: @@ -780,7 +762,7 @@ def write_u16(self, address: int, value: int | list[int]) -> ReplyHarpMessage: address=address, payload_type=PayloadType.U16, value=value, - ).frame + ) ) def write_s16(self, address: int, value: int | list[int]) -> ReplyHarpMessage: @@ -805,7 +787,7 @@ def write_s16(self, address: int, value: int | list[int]) -> ReplyHarpMessage: address=address, payload_type=PayloadType.S16, value=value, - ).frame + ) ) def write_u32(self, address: int, value: int | list[int]) -> ReplyHarpMessage: @@ -830,7 +812,7 @@ def write_u32(self, address: int, value: int | list[int]) -> ReplyHarpMessage: address=address, payload_type=PayloadType.U32, value=value, - ).frame + ) ) def write_s32(self, address: int, value: int | list[int]) -> ReplyHarpMessage: @@ -855,7 +837,7 @@ def write_s32(self, address: int, value: int | list[int]) -> ReplyHarpMessage: address=address, payload_type=PayloadType.S32, value=value, - ).frame + ) ) def write_u64(self, address: int, value: int | list[int]) -> ReplyHarpMessage: @@ -880,7 +862,7 @@ def write_u64(self, address: int, value: int | list[int]) -> ReplyHarpMessage: address=address, payload_type=PayloadType.U64, value=value, - ).frame + ) ) def write_s64(self, address: int, value: int | list[int]) -> ReplyHarpMessage: @@ -905,7 +887,7 @@ def write_s64(self, address: int, value: int | list[int]) -> ReplyHarpMessage: address=address, payload_type=PayloadType.S64, value=value, - ).frame + ) ) def write_float(self, address: int, value: float | list[float]) -> ReplyHarpMessage: @@ -930,7 +912,7 @@ def write_float(self, address: int, value: float | list[float]) -> ReplyHarpMess address=address, payload_type=PayloadType.Float, value=value, - ).frame + ) ) def _read_who_am_i(self) -> int: @@ -945,7 +927,7 @@ def _read_who_am_i(self) -> int: address = CommonRegisters.WHO_AM_I reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U16).frame + HarpMessage.create(MessageType.READ, address, PayloadType.U16) ) return reply.payload_as_int() @@ -973,7 +955,7 @@ def _read_hw_version_h(self) -> int: address = CommonRegisters.HW_VERSION_H reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame + HarpMessage.create(MessageType.READ, address, PayloadType.U8) ) return reply.payload_as_int() @@ -990,7 +972,7 @@ def _read_hw_version_l(self) -> int: address = CommonRegisters.HW_VERSION_L reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame + HarpMessage.create(MessageType.READ, address, PayloadType.U8) ) return reply.payload_as_int() @@ -1007,7 +989,7 @@ def _read_assembly_version(self) -> int: address = CommonRegisters.ASSEMBLY_VERSION reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame + HarpMessage.create(MessageType.READ, address, PayloadType.U8) ) return reply.payload_as_int() @@ -1024,7 +1006,7 @@ def _read_harp_version_h(self) -> int: address = CommonRegisters.HARP_VERSION_H reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame + HarpMessage.create(MessageType.READ, address, PayloadType.U8) ) return reply.payload_as_int() @@ -1041,7 +1023,7 @@ def _read_harp_version_l(self) -> int: address = CommonRegisters.HARP_VERSION_L reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame + HarpMessage.create(MessageType.READ, address, PayloadType.U8) ) return reply.payload_as_int() @@ -1058,7 +1040,7 @@ def _read_fw_version_h(self) -> int: address = CommonRegisters.FIRMWARE_VERSION_H reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame + HarpMessage.create(MessageType.READ, address, PayloadType.U8) ) return reply.payload_as_int() @@ -1075,7 +1057,7 @@ def _read_fw_version_l(self) -> int: address = CommonRegisters.FIRMWARE_VERSION_L reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame + HarpMessage.create(MessageType.READ, address, PayloadType.U8) ) return reply.payload_as_int() @@ -1092,7 +1074,7 @@ def _read_device_name(self) -> str: address = CommonRegisters.DEVICE_NAME reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame + HarpMessage.create(MessageType.READ, address, PayloadType.U8) ) return reply.payload_as_string() @@ -1109,7 +1091,7 @@ def _read_serial_number(self) -> int: address = CommonRegisters.SERIAL_NUMBER reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame + HarpMessage.create(MessageType.READ, address, PayloadType.U8) ) if reply.is_error(): @@ -1129,7 +1111,7 @@ def _read_clock_config(self) -> int: address = CommonRegisters.CLOCK_CONFIG reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame + HarpMessage.create(MessageType.READ, address, PayloadType.U8) ) return reply.payload_as_int() @@ -1146,7 +1128,7 @@ def _read_timestamp_offset(self) -> int: address = CommonRegisters.TIMESTAMP_OFFSET reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8).frame + HarpMessage.create(MessageType.READ, address, PayloadType.U8) ) return reply.payload_as_int() diff --git a/pyharp/drivers/behavior.py b/pyharp/drivers/behavior.py index 2f4b333..04aa2d4 100644 --- a/pyharp/drivers/behavior.py +++ b/pyharp/drivers/behavior.py @@ -124,7 +124,7 @@ def disable_all_events(self) -> ReplyHarpMessage: (1 << Events.cam1.value) ) ^ 0xFF) reg_type, reg_index, _ = REGISTERS["EVNT_ENABLE"] return self.device.send( - WriteHarpMessage(reg_type, reg_index, event_reg_bitmask).frame + WriteHarpMessage(reg_type, reg_index, event_reg_bitmask) ) @@ -135,7 +135,7 @@ def enable_events(self, *events: Events) -> ReplyHarpMessage: event_reg_bitmask |= (1 << event.value) reg_type, reg_index, _ = REGISTERS["EVNT_ENABLE"] return self.device.send( - WriteHarpMessage(reg_type, reg_index, event_reg_bitmask).frame + WriteHarpMessage(reg_type, reg_index, event_reg_bitmask) ) @@ -145,9 +145,7 @@ def enable_events(self, *events: Events) -> ReplyHarpMessage: def all_input_states(self): """return the state of all PORT digital inputs.""" reg_type, reg_index, _ = REGISTERS["PORT_DIS"] - return self.device.send( - ReadHarpMessage(reg_type, reg_index).frame - ).payload_as_int() + return self.device.send(ReadHarpMessage(reg_type, reg_index)).payload @property def DI0(self): @@ -173,14 +171,14 @@ def DI2(self): # """set the state of all PORT digital ios. (1 is output.)""" # reg_type, reg_index, _ = REGISTERS["PORT_DIOS_CONF"] - # self.device.send(WriteHarpMessage(reg_type, reg_index, bitmask).frame) + # self.device.send(WriteHarpMessage(reg_type, reg_index, bitmask)) # # @property # def all_io_states(self): # """return the state of all PORT digital ios.""" # reg_type, reg_index, _ = REGISTERS["PORT_DIOS_IN"] # read_message_type = self.__class__.READ_MSG_LOOKUP[reg_type] -# return self.device.send(read_message_type(reg_index).frame).payload_as_int() + # return self.device.send(read_message_type(reg_index)).payload # # @all_io_states.setter # def all_io_states(self, bitmask : int): @@ -189,19 +187,19 @@ def DI2(self): # # _IN register, which is different from the OUTPUT # reg_type, reg_index, _ = REGISTERS["PORT_DIOS_IN"] - # return self.device.send(WriteHarpMessage(reg_type, reg_index, bitmask).frame) + # return self.device.send(WriteHarpMessage(reg_type, reg_index, bitmask)) # # def set_io_outputs(self, bitmask : int): # """set digital input/outputs to logic 1 according to bitmask.""" # reg_type, reg_index, _ = REGISTERS["PORT_DIOS_SET"] - # return self.device.send(WriteHarpMessage(reg_type, reg_index, bitmask).frame) + # return self.device.send(WriteHarpMessage(reg_type, reg_index, bitmask)) # # def clear_io_outputs(self, bitmask : int): # """clear digital input/outputs (specified with logic 1) according to bitmask.""" # reg_type, reg_index, _ = REGISTERS["PORT_DIOS_CLEAR"] - # return self.device.send(WriteHarpMessage(reg_type, reg_index, bitmask).frame) + # return self.device.send(WriteHarpMessage(reg_type, reg_index, bitmask)) # # @property # def port0_io0(self): @@ -239,25 +237,23 @@ def DI2(self): def all_output_states(self): """return the state of all PORT digital inputs.""" reg_type, reg_index, _ = REGISTERS["OUTPUTS_OUT"] - return self.device.send( - ReadHarpMessage(reg_type, reg_index).frame - ).payload_as_int() + return self.device.send(ReadHarpMessage(reg_type, reg_index)).payload @all_output_states.setter def all_output_states(self, bitmask : int): """set the state of all PORT digital inputs.""" reg_type, reg_index, _ = REGISTERS["OUTPUTS_OUT"] - return self.device.send(WriteHarpMessage(reg_type, reg_index, bitmask).frame) + return self.device.send(WriteHarpMessage(reg_type, reg_index, bitmask)) def set_outputs(self, bitmask : int): """set digital outputs to logic 1 according to bitmask.""" reg_type, reg_index, _ = REGISTERS["OUTPUTS_SET"] - return self.device.send(WriteHarpMessage(reg_type, reg_index, bitmask).frame) + return self.device.send(WriteHarpMessage(reg_type, reg_index, bitmask)) def clear_outputs(self, bitmask : int): """clear digital outputs (specified with logic 1) according to bitmask.""" reg_type, reg_index, _ = REGISTERS["OUTPUTS_CLR"] - return self.device.send(WriteHarpMessage(reg_type, reg_index, bitmask).frame) + return self.device.send(WriteHarpMessage(reg_type, reg_index, bitmask)) @property def D0(self): diff --git a/tests/test_device.py b/tests/test_device.py index d511549..7e8292c 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -25,7 +25,7 @@ # read_size: int = 35 # TODO: automatically calculate this! # reply: ReplyHarpMessage = device.send( -# HarpMessage.create(MessageType.READ, register, PayloadType.U8).frame +# HarpMessage.create(MessageType.READ, register, PayloadType.U8) # ) # assert reply is not None # # assert reply.payload_as_int() == write_value @@ -50,7 +50,7 @@ # reply = device.send( # HarpMessage.create( # MessageType.WRITE, register, PayloadType.U8, write_value -# ).frame +# ) # ) # assert reply is not None From 6b69b65090a4207a069474f98193975c905c07f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 23 Apr 2025 16:56:00 +0100 Subject: [PATCH 104/267] Remove unused methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- pyharp/device.py | 22 +++++++++++----------- pyharp/messages.py | 24 ------------------------ tests/test_device.py | 2 +- 3 files changed, 12 insertions(+), 36 deletions(-) diff --git a/pyharp/device.py b/pyharp/device.py index a6d10f2..aa1c1cf 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -930,7 +930,7 @@ def _read_who_am_i(self) -> int: HarpMessage.create(MessageType.READ, address, PayloadType.U16) ) - return reply.payload_as_int() + return reply.payload def _read_default_device_name(self) -> str: """ @@ -958,7 +958,7 @@ def _read_hw_version_h(self) -> int: HarpMessage.create(MessageType.READ, address, PayloadType.U8) ) - return reply.payload_as_int() + return reply.payload def _read_hw_version_l(self) -> int: """ @@ -975,7 +975,7 @@ def _read_hw_version_l(self) -> int: HarpMessage.create(MessageType.READ, address, PayloadType.U8) ) - return reply.payload_as_int() + return reply.payload def _read_assembly_version(self) -> int: """ @@ -992,7 +992,7 @@ def _read_assembly_version(self) -> int: HarpMessage.create(MessageType.READ, address, PayloadType.U8) ) - return reply.payload_as_int() + return reply.payload def _read_harp_version_h(self) -> int: """ @@ -1009,7 +1009,7 @@ def _read_harp_version_h(self) -> int: HarpMessage.create(MessageType.READ, address, PayloadType.U8) ) - return reply.payload_as_int() + return reply.payload def _read_harp_version_l(self) -> int: """ @@ -1026,7 +1026,7 @@ def _read_harp_version_l(self) -> int: HarpMessage.create(MessageType.READ, address, PayloadType.U8) ) - return reply.payload_as_int() + return reply.payload def _read_fw_version_h(self) -> int: """ @@ -1043,7 +1043,7 @@ def _read_fw_version_h(self) -> int: HarpMessage.create(MessageType.READ, address, PayloadType.U8) ) - return reply.payload_as_int() + return reply.payload def _read_fw_version_l(self) -> int: """ @@ -1060,7 +1060,7 @@ def _read_fw_version_l(self) -> int: HarpMessage.create(MessageType.READ, address, PayloadType.U8) ) - return reply.payload_as_int() + return reply.payload def _read_device_name(self) -> str: """ @@ -1097,7 +1097,7 @@ def _read_serial_number(self) -> int: if reply.is_error(): return 0 - return reply.payload_as_int() + return reply.payload def _read_clock_config(self) -> int: """ @@ -1114,7 +1114,7 @@ def _read_clock_config(self) -> int: HarpMessage.create(MessageType.READ, address, PayloadType.U8) ) - return reply.payload_as_int() + return reply.payload def _read_timestamp_offset(self) -> int: """ @@ -1131,7 +1131,7 @@ def _read_timestamp_offset(self) -> int: HarpMessage.create(MessageType.READ, address, PayloadType.U8) ) - return reply.payload_as_int() + return reply.payload def __enter__(self): """ diff --git a/pyharp/messages.py b/pyharp/messages.py index 023ff41..54b86c3 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -463,18 +463,6 @@ def timestamp(self) -> float: """ return self._timestamp - # TODO: does this function makes sense since self.payload() already exists? - def payload_as_int(self) -> int: - """ - Returns the payload as an int. - - Returns - ------- - int - the payload parsed as an int - """ - return self._raw_payload[0] - def payload_as_string(self) -> str: """ Returns the payload as a str. @@ -486,18 +474,6 @@ def payload_as_string(self) -> str: """ return self._raw_payload.decode("utf-8").rstrip("\x00") - # TODO: handle float case and/or delete functional altogether - def payload_as_float(self) -> float: - """ - Returns the payload as a float. - - Returns - ------- - float - the payload parsed as a float - """ - return self.payload[0] - class ReadHarpMessage(HarpMessage): """ diff --git a/tests/test_device.py b/tests/test_device.py index 7e8292c..a0137ae 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -28,7 +28,7 @@ # HarpMessage.create(MessageType.READ, register, PayloadType.U8) # ) # assert reply is not None -# # assert reply.payload_as_int() == write_value +# # assert reply.payload == write_value # print(reply) # assert device._dump_file_path.exists() From 897ffd3cbc321ab5140b2e069913a37620b533db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 23 Apr 2025 16:57:04 +0100 Subject: [PATCH 105/267] Fix bug with uninitialized fields and wrong property call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- pyharp/device.py | 4 ++-- pyharp/messages.py | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/pyharp/device.py b/pyharp/device.py index aa1c1cf..7de2ec9 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -61,7 +61,7 @@ class Device: _ser: HarpSerial _dump_file_path: Path - _dump_file: Optional[BufferedWriter] + _dump_file: Optional[BufferedWriter] = None _read_timeout_s: float _TIMEOUT_S: float = 1.0 @@ -1094,7 +1094,7 @@ def _read_serial_number(self) -> int: HarpMessage.create(MessageType.READ, address, PayloadType.U8) ) - if reply.is_error(): + if reply.is_error: return 0 return reply.payload diff --git a/pyharp/messages.py b/pyharp/messages.py index 54b86c3..c914c52 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -30,11 +30,8 @@ class HarpMessage: DEFAULT_PORT: int = 255 BASE_LENGTH: int = 4 - _frame: bytearray - - def __init__(self): - self._frame = bytearray() - self._port = self.DEFAULT_PORT + _frame: bytearray = bytearray() + _port: int = DEFAULT_PORT def calculate_checksum(self) -> int: """ From 9dfcf15c83dddfa0746463c897924e99794f45ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 30 Apr 2025 10:25:45 +0100 Subject: [PATCH 106/267] Fix bug on Device's send method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- pyharp/device.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyharp/device.py b/pyharp/device.py index 7de2ec9..f6c9356 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -435,11 +435,11 @@ def send(self, message: HarpMessage) -> Optional[ReplyHarpMessage]: Optional[ReplyHarpMessage] the reply to the Harp message or None if no reply is given """ - self._ser.write(message) + self._ser.write(message.frame) reply = self._read() - self._dump_reply(reply) + self._dump_reply(reply.frame) return reply From 2fd965fa00b89a5ff8c4e84e2820df7014ae9c1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 30 Apr 2025 10:26:09 +0100 Subject: [PATCH 107/267] Handle empty replies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- pyharp/device.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyharp/device.py b/pyharp/device.py index f6c9356..ebc94ea 100644 --- a/pyharp/device.py +++ b/pyharp/device.py @@ -438,6 +438,8 @@ def send(self, message: HarpMessage) -> Optional[ReplyHarpMessage]: self._ser.write(message.frame) reply = self._read() + if reply is None: + return None self._dump_reply(reply.frame) From 4e3ee84bb6c37eba21c0187cc6ba83bc70a01432 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 30 Apr 2025 10:26:27 +0100 Subject: [PATCH 108/267] Fix bug with str method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- pyharp/messages.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pyharp/messages.py b/pyharp/messages.py index c914c52..e0bb046 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -383,15 +383,24 @@ def __str__(self) -> str: payload_str = "".join( f"{item:{format_str}} " - for item in (self.payload if self.payload is list else [self.payload]) + for item in ( + self.payload if isinstance(self.payload, list) else [self.payload] + ) ) + # Check if the object has a 'timestamp' property and it's not None + timestamp_line = "" + if hasattr(self, "timestamp"): + ts = getattr(self, "timestamp") + if ts is not None: + timestamp_line = f"Timestamp: {ts}\r\n" + return ( f"Type: {self.message_type.name}\r\n" + f"Length: {self.length}\r\n" + f"Address: {self.address}\r\n" + f"Port: {self.port}\r\n" - + f"Timestamp: {self.timestamp}\r\n" + + timestamp_line + f"Payload Type: {self.payload_type.name}\r\n" + f"Payload Length: {len(self.payload) if self.payload is list else 1}\r\n" + f"Payload: {payload_str}\r\n" From f310406ffd9c91062bb037381e5467b139671db1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Tue, 20 May 2025 09:37:57 +0100 Subject: [PATCH 109/267] Reformat --- .gitignore | 2 +- .vscode/extensions.json | 10 +++++----- docs/api/core.md | 2 +- docs/api/device.md | 2 +- docs/api/messages.md | 2 +- docs/assets/logo.svg | 14 +++++++------- docs/examples/index.md | 2 +- .../olfactometer_example/olfactometer_example.md | 2 +- .../read_and_write_from_registers.md | 2 +- docs/examples/wait_for_events/wait_for_events.md | 2 +- docs/stylesheets/extra.css | 2 +- pyharp/base.py | 3 ++- 12 files changed, 23 insertions(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index 1800114..0a19790 100644 --- a/.gitignore +++ b/.gitignore @@ -171,4 +171,4 @@ cython_debug/ .ruff_cache/ # PyPI configuration file -.pypirc \ No newline at end of file +.pypirc diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 4e265ea..2faf153 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,6 +1,6 @@ { - "recommendations": [ - "ms-python.python", - "charliermarsh.ruff" - ] -} \ No newline at end of file + "recommendations": [ + "ms-python.python", + "charliermarsh.ruff" + ] +} diff --git a/docs/api/core.md b/docs/api/core.md index e3d8970..6ec54e7 100644 --- a/docs/api/core.md +++ b/docs/api/core.md @@ -1,4 +1,4 @@ ::: pyharp.MessageType ::: pyharp.PayloadType ::: pyharp.CommonRegisters -::: pyharp.OperationMode \ No newline at end of file +::: pyharp.OperationMode diff --git a/docs/api/device.md b/docs/api/device.md index 9e949dc..842f814 100644 --- a/docs/api/device.md +++ b/docs/api/device.md @@ -1 +1 @@ -::: pyharp.device.Device \ No newline at end of file +::: pyharp.device.Device diff --git a/docs/api/messages.md b/docs/api/messages.md index 17f4233..9d85270 100644 --- a/docs/api/messages.md +++ b/docs/api/messages.md @@ -1,4 +1,4 @@ ::: pyharp.messages.HarpMessage ::: pyharp.messages.ReplyHarpMessage ::: pyharp.messages.ReadHarpMessage -::: pyharp.messages.WriteHarpMessage \ No newline at end of file +::: pyharp.messages.WriteHarpMessage diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg index acb0503..96224d9 100644 --- a/docs/assets/logo.svg +++ b/docs/assets/logo.svg @@ -56,16 +56,16 @@ id="g4" transform="translate(0,326)" style="fill:#ffffff"> - - - - - - + + + + + + \ No newline at end of file + sodipodi:nodetypes="cccccssscccsssssccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccsssssccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccsssss" /> diff --git a/docs/examples/index.md b/docs/examples/index.md index 695565a..f66468a 100644 --- a/docs/examples/index.md +++ b/docs/examples/index.md @@ -7,4 +7,4 @@ Here's the complete list of available examples: - [Getting Device Info](./get_info/get_info.md) - connect to a Harp device and read its information. - [Wait for Events](./wait_for_events/wait_for_events.md) - connect to a Harp device and wait for events. - [Write and Read from Registers](./read_and_write_from_registers/read_and_write_from_registers.md) - connect to the Harp Behavior, read from a digital input and write to a digital output. -- [Olfactometer Example](./olfactometer_example/olfactometer_example.md) - connect to the Harp Olfactometer, enable flow, open and close the odor valves and monitor the measured flow values. \ No newline at end of file +- [Olfactometer Example](./olfactometer_example/olfactometer_example.md) - connect to the Harp Olfactometer, enable flow, open and close the odor valves and monitor the measured flow values. diff --git a/docs/examples/olfactometer_example/olfactometer_example.md b/docs/examples/olfactometer_example/olfactometer_example.md index f78d2e6..91b1024 100644 --- a/docs/examples/olfactometer_example/olfactometer_example.md +++ b/docs/examples/olfactometer_example/olfactometer_example.md @@ -1,6 +1,6 @@ # Olfactometer Example -This example shows how to interface with the [Harp Olfactometer](https://github.com/harp-tech/device.olfactometer). +This example shows how to interface with the [Harp Olfactometer](https://github.com/harp-tech/device.olfactometer). In this example, the flows for the different channels are enabled to random flow values, then every odor valve is opened, one at a time every 5 seconds, and finally the flow is disabled before closing the connection with the device. During this time, the actual flows in every channel are being printed out in the terminal. diff --git a/docs/examples/read_and_write_from_registers/read_and_write_from_registers.md b/docs/examples/read_and_write_from_registers/read_and_write_from_registers.md index ea819d9..040f6e4 100644 --- a/docs/examples/read_and_write_from_registers/read_and_write_from_registers.md +++ b/docs/examples/read_and_write_from_registers/read_and_write_from_registers.md @@ -14,4 +14,4 @@ This example demonstrates how to read and write from registers. In this particul ## Schematics !!! warning - _TODO_ \ No newline at end of file + _TODO_ diff --git a/docs/examples/wait_for_events/wait_for_events.md b/docs/examples/wait_for_events/wait_for_events.md index c4aa6a0..8db54fa 100644 --- a/docs/examples/wait_for_events/wait_for_events.md +++ b/docs/examples/wait_for_events/wait_for_events.md @@ -9,4 +9,4 @@ This example demonstrates how to read the events sent by the Harp device. ```python [](./wait_for_events.py) ``` - \ No newline at end of file + diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 5829909..b417550 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -6,4 +6,4 @@ .md-nav__title { display: none; -} \ No newline at end of file +} diff --git a/pyharp/base.py b/pyharp/base.py index 0d5d3f0..fe3ad59 100644 --- a/pyharp/base.py +++ b/pyharp/base.py @@ -178,6 +178,7 @@ class OperationMode(IntEnum): RESERVED = 2 SPEED = 3 + class OperationCtrl(IntFlag): """ An enumeration with the operation control bits of a Harp device. More information on the operation control bits can be found [here](https://harp-tech.org/protocol/Device.html#r_operation_ctrl-u16--operation-mode-configuration). @@ -272,4 +273,4 @@ class ClockConfig(IntFlag): REP_ABLE = 0x08 GEN_ABLE = 0x10 CLK_UNLOCK = 0x40 - CLK_LOCK = 0x80 \ No newline at end of file + CLK_LOCK = 0x80 From 721fcb4e7140a0608ae6405c113346d39745345a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Tue, 20 May 2025 09:38:10 +0100 Subject: [PATCH 110/267] Update pre-commit config --- .pre-commit-config.yaml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..23d8811 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,22 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: "v5.0.0" + hooks: + - id: check-case-conflict + - id: check-merge-conflict + - id: check-toml + - id: check-yaml + - id: check-json + exclude: ^.devcontainer/devcontainer.json + - id: pretty-format-json + exclude: ^.devcontainer/devcontainer.json + args: [--autofix, --no-sort-keys] + - id: end-of-file-fixer + - id: trailing-whitespace + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: "v0.11.5" + hooks: + - id: ruff + args: [--exit-non-zero-on-fix] + - id: ruff-format From d6a886a15f0e7da20cb46c3597974c527e2430e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Tue, 20 May 2025 09:39:10 +0100 Subject: [PATCH 111/267] Add monorepo-plugin to mkdocs --- mkdocs.yml | 6 +- pyproject.toml | 1 + uv.lock | 664 ++++++++++++++++++++++++++----------------------- 3 files changed, 355 insertions(+), 316 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 85e4ba7..19c56c7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,6 +6,7 @@ plugins: - search - autorefs - codeinclude + - monorepo - mkdocstrings: handlers: python: @@ -18,7 +19,7 @@ plugins: repository: fchampalimaud/pyharp branch: main - git-authors - + markdown_extensions: - abbr @@ -84,6 +85,7 @@ nav: - Core: api/core.md - Device: api/device.md - Messages: api/messages.md + - Devices: '*include ./pyharp.devices/*/mkdocs.yml' extra_css: -- stylesheets/extra.css \ No newline at end of file +- stylesheets/extra.css diff --git a/pyproject.toml b/pyproject.toml index 0e23e36..666bc37 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ dev = [ "mkdocs-git-authors-plugin>=0.9.4", "mkdocs-git-committers-plugin-2>=2.5.0", "mkdocs-material>=9.6.9", + "mkdocs-monorepo-plugin>=1.1.0", "mkdocstrings-python>=1.16.6", "pytest>=8.3.5", "pytest-cov>=6.1.1", diff --git a/uv.lock b/uv.lock index 9a082b9..de644e6 100644 --- a/uv.lock +++ b/uv.lock @@ -1,84 +1,84 @@ version = 1 -revision = 1 +revision = 2 requires-python = ">=3.11" [[package]] name = "babel" version = "2.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852 } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537 }, + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, ] [[package]] name = "backrefs" version = "5.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/46/caba1eb32fa5784428ab401a5487f73db4104590ecd939ed9daaf18b47e0/backrefs-5.8.tar.gz", hash = "sha256:2cab642a205ce966af3dd4b38ee36009b31fa9502a35fd61d59ccc116e40a6bd", size = 6773994 } +sdist = { url = "https://files.pythonhosted.org/packages/6c/46/caba1eb32fa5784428ab401a5487f73db4104590ecd939ed9daaf18b47e0/backrefs-5.8.tar.gz", hash = "sha256:2cab642a205ce966af3dd4b38ee36009b31fa9502a35fd61d59ccc116e40a6bd", size = 6773994, upload-time = "2025-02-25T18:15:32.003Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/cb/d019ab87fe70e0fe3946196d50d6a4428623dc0c38a6669c8cae0320fbf3/backrefs-5.8-py310-none-any.whl", hash = "sha256:c67f6638a34a5b8730812f5101376f9d41dc38c43f1fdc35cb54700f6ed4465d", size = 380337 }, - { url = "https://files.pythonhosted.org/packages/a9/86/abd17f50ee21b2248075cb6924c6e7f9d23b4925ca64ec660e869c2633f1/backrefs-5.8-py311-none-any.whl", hash = "sha256:2e1c15e4af0e12e45c8701bd5da0902d326b2e200cafcd25e49d9f06d44bb61b", size = 392142 }, - { url = "https://files.pythonhosted.org/packages/b3/04/7b415bd75c8ab3268cc138c76fa648c19495fcc7d155508a0e62f3f82308/backrefs-5.8-py312-none-any.whl", hash = "sha256:bbef7169a33811080d67cdf1538c8289f76f0942ff971222a16034da88a73486", size = 398021 }, - { url = "https://files.pythonhosted.org/packages/04/b8/60dcfb90eb03a06e883a92abbc2ab95c71f0d8c9dd0af76ab1d5ce0b1402/backrefs-5.8-py313-none-any.whl", hash = "sha256:e3a63b073867dbefd0536425f43db618578528e3896fb77be7141328642a1585", size = 399915 }, - { url = "https://files.pythonhosted.org/packages/0c/37/fb6973edeb700f6e3d6ff222400602ab1830446c25c7b4676d8de93e65b8/backrefs-5.8-py39-none-any.whl", hash = "sha256:a66851e4533fb5b371aa0628e1fee1af05135616b86140c9d787a2ffdf4b8fdc", size = 380336 }, + { url = "https://files.pythonhosted.org/packages/bf/cb/d019ab87fe70e0fe3946196d50d6a4428623dc0c38a6669c8cae0320fbf3/backrefs-5.8-py310-none-any.whl", hash = "sha256:c67f6638a34a5b8730812f5101376f9d41dc38c43f1fdc35cb54700f6ed4465d", size = 380337, upload-time = "2025-02-25T16:53:14.607Z" }, + { url = "https://files.pythonhosted.org/packages/a9/86/abd17f50ee21b2248075cb6924c6e7f9d23b4925ca64ec660e869c2633f1/backrefs-5.8-py311-none-any.whl", hash = "sha256:2e1c15e4af0e12e45c8701bd5da0902d326b2e200cafcd25e49d9f06d44bb61b", size = 392142, upload-time = "2025-02-25T16:53:17.266Z" }, + { url = "https://files.pythonhosted.org/packages/b3/04/7b415bd75c8ab3268cc138c76fa648c19495fcc7d155508a0e62f3f82308/backrefs-5.8-py312-none-any.whl", hash = "sha256:bbef7169a33811080d67cdf1538c8289f76f0942ff971222a16034da88a73486", size = 398021, upload-time = "2025-02-25T16:53:26.378Z" }, + { url = "https://files.pythonhosted.org/packages/04/b8/60dcfb90eb03a06e883a92abbc2ab95c71f0d8c9dd0af76ab1d5ce0b1402/backrefs-5.8-py313-none-any.whl", hash = "sha256:e3a63b073867dbefd0536425f43db618578528e3896fb77be7141328642a1585", size = 399915, upload-time = "2025-02-25T16:53:28.167Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/fb6973edeb700f6e3d6ff222400602ab1830446c25c7b4676d8de93e65b8/backrefs-5.8-py39-none-any.whl", hash = "sha256:a66851e4533fb5b371aa0628e1fee1af05135616b86140c9d787a2ffdf4b8fdc", size = 380336, upload-time = "2025-02-25T16:53:29.858Z" }, ] [[package]] name = "certifi" version = "2025.1.31" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577, upload-time = "2025-01-31T02:16:47.166Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393, upload-time = "2025-01-31T02:16:45.015Z" }, ] [[package]] name = "charset-normalizer" version = "3.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 }, - { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 }, - { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 }, - { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 }, - { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 }, - { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 }, - { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 }, - { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 }, - { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 }, - { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 }, - { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 }, - { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 }, - { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 }, - { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, - { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, - { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, - { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, - { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, - { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, - { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, - { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, - { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, - { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, - { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, - { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, - { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, - { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, - { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, - { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, - { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, - { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, - { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, - { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, - { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, - { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, - { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, - { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, - { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, - { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, - { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188, upload-time = "2024-12-24T18:12:35.43Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995, upload-time = "2024-12-24T18:10:12.838Z" }, + { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471, upload-time = "2024-12-24T18:10:14.101Z" }, + { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831, upload-time = "2024-12-24T18:10:15.512Z" }, + { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335, upload-time = "2024-12-24T18:10:18.369Z" }, + { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862, upload-time = "2024-12-24T18:10:19.743Z" }, + { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673, upload-time = "2024-12-24T18:10:21.139Z" }, + { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211, upload-time = "2024-12-24T18:10:22.382Z" }, + { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039, upload-time = "2024-12-24T18:10:24.802Z" }, + { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939, upload-time = "2024-12-24T18:10:26.124Z" }, + { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075, upload-time = "2024-12-24T18:10:30.027Z" }, + { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340, upload-time = "2024-12-24T18:10:32.679Z" }, + { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205, upload-time = "2024-12-24T18:10:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441, upload-time = "2024-12-24T18:10:37.574Z" }, + { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105, upload-time = "2024-12-24T18:10:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404, upload-time = "2024-12-24T18:10:44.272Z" }, + { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423, upload-time = "2024-12-24T18:10:45.492Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184, upload-time = "2024-12-24T18:10:47.898Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268, upload-time = "2024-12-24T18:10:50.589Z" }, + { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601, upload-time = "2024-12-24T18:10:52.541Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098, upload-time = "2024-12-24T18:10:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520, upload-time = "2024-12-24T18:10:55.048Z" }, + { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852, upload-time = "2024-12-24T18:10:57.647Z" }, + { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488, upload-time = "2024-12-24T18:10:59.43Z" }, + { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192, upload-time = "2024-12-24T18:11:00.676Z" }, + { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550, upload-time = "2024-12-24T18:11:01.952Z" }, + { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785, upload-time = "2024-12-24T18:11:03.142Z" }, + { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698, upload-time = "2024-12-24T18:11:05.834Z" }, + { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162, upload-time = "2024-12-24T18:11:07.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263, upload-time = "2024-12-24T18:11:08.374Z" }, + { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966, upload-time = "2024-12-24T18:11:09.831Z" }, + { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992, upload-time = "2024-12-24T18:11:12.03Z" }, + { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162, upload-time = "2024-12-24T18:11:13.372Z" }, + { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972, upload-time = "2024-12-24T18:11:14.628Z" }, + { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095, upload-time = "2024-12-24T18:11:17.672Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668, upload-time = "2024-12-24T18:11:18.989Z" }, + { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073, upload-time = "2024-12-24T18:11:21.507Z" }, + { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732, upload-time = "2024-12-24T18:11:22.774Z" }, + { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391, upload-time = "2024-12-24T18:11:24.139Z" }, + { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702, upload-time = "2024-12-24T18:11:26.535Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767, upload-time = "2024-12-24T18:12:32.852Z" }, ] [[package]] @@ -88,68 +88,68 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "coverage" version = "7.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/77/074d201adb8383addae5784cb8e2dac60bb62bfdf28b2b10f3a3af2fda47/coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27", size = 211493 }, - { url = "https://files.pythonhosted.org/packages/a9/89/7a8efe585750fe59b48d09f871f0e0c028a7b10722b2172dfe021fa2fdd4/coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea", size = 211921 }, - { url = "https://files.pythonhosted.org/packages/e9/ef/96a90c31d08a3f40c49dbe897df4f1fd51fb6583821a1a1c5ee30cc8f680/coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7", size = 244556 }, - { url = "https://files.pythonhosted.org/packages/89/97/dcd5c2ce72cee9d7b0ee8c89162c24972fb987a111b92d1a3d1d19100c61/coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040", size = 242245 }, - { url = "https://files.pythonhosted.org/packages/b2/7b/b63cbb44096141ed435843bbb251558c8e05cc835c8da31ca6ffb26d44c0/coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543", size = 244032 }, - { url = "https://files.pythonhosted.org/packages/97/e3/7fa8c2c00a1ef530c2a42fa5df25a6971391f92739d83d67a4ee6dcf7a02/coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2", size = 243679 }, - { url = "https://files.pythonhosted.org/packages/4f/b3/e0a59d8df9150c8a0c0841d55d6568f0a9195692136c44f3d21f1842c8f6/coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318", size = 241852 }, - { url = "https://files.pythonhosted.org/packages/9b/82/db347ccd57bcef150c173df2ade97976a8367a3be7160e303e43dd0c795f/coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9", size = 242389 }, - { url = "https://files.pythonhosted.org/packages/21/f6/3f7d7879ceb03923195d9ff294456241ed05815281f5254bc16ef71d6a20/coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c", size = 213997 }, - { url = "https://files.pythonhosted.org/packages/28/87/021189643e18ecf045dbe1e2071b2747901f229df302de01c998eeadf146/coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78", size = 214911 }, - { url = "https://files.pythonhosted.org/packages/aa/12/4792669473297f7973518bec373a955e267deb4339286f882439b8535b39/coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc", size = 211684 }, - { url = "https://files.pythonhosted.org/packages/be/e1/2a4ec273894000ebedd789e8f2fc3813fcaf486074f87fd1c5b2cb1c0a2b/coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6", size = 211935 }, - { url = "https://files.pythonhosted.org/packages/f8/3a/7b14f6e4372786709a361729164125f6b7caf4024ce02e596c4a69bccb89/coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d", size = 245994 }, - { url = "https://files.pythonhosted.org/packages/54/80/039cc7f1f81dcbd01ea796d36d3797e60c106077e31fd1f526b85337d6a1/coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05", size = 242885 }, - { url = "https://files.pythonhosted.org/packages/10/e0/dc8355f992b6cc2f9dcd5ef6242b62a3f73264893bc09fbb08bfcab18eb4/coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a", size = 245142 }, - { url = "https://files.pythonhosted.org/packages/43/1b/33e313b22cf50f652becb94c6e7dae25d8f02e52e44db37a82de9ac357e8/coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6", size = 244906 }, - { url = "https://files.pythonhosted.org/packages/05/08/c0a8048e942e7f918764ccc99503e2bccffba1c42568693ce6955860365e/coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47", size = 243124 }, - { url = "https://files.pythonhosted.org/packages/5b/62/ea625b30623083c2aad645c9a6288ad9fc83d570f9adb913a2abdba562dd/coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe", size = 244317 }, - { url = "https://files.pythonhosted.org/packages/62/cb/3871f13ee1130a6c8f020e2f71d9ed269e1e2124aa3374d2180ee451cee9/coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545", size = 214170 }, - { url = "https://files.pythonhosted.org/packages/88/26/69fe1193ab0bfa1eb7a7c0149a066123611baba029ebb448500abd8143f9/coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b", size = 214969 }, - { url = "https://files.pythonhosted.org/packages/f3/21/87e9b97b568e223f3438d93072479c2f36cc9b3f6b9f7094b9d50232acc0/coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd", size = 211708 }, - { url = "https://files.pythonhosted.org/packages/75/be/882d08b28a0d19c9c4c2e8a1c6ebe1f79c9c839eb46d4fca3bd3b34562b9/coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00", size = 211981 }, - { url = "https://files.pythonhosted.org/packages/7a/1d/ce99612ebd58082fbe3f8c66f6d8d5694976c76a0d474503fa70633ec77f/coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64", size = 245495 }, - { url = "https://files.pythonhosted.org/packages/dc/8d/6115abe97df98db6b2bd76aae395fcc941d039a7acd25f741312ced9a78f/coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067", size = 242538 }, - { url = "https://files.pythonhosted.org/packages/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008", size = 244561 }, - { url = "https://files.pythonhosted.org/packages/22/70/c10c77cd77970ac965734fe3419f2c98665f6e982744a9bfb0e749d298f4/coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733", size = 244633 }, - { url = "https://files.pythonhosted.org/packages/38/5a/4f7569d946a07c952688debee18c2bb9ab24f88027e3d71fd25dbc2f9dca/coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323", size = 242712 }, - { url = "https://files.pythonhosted.org/packages/bb/a1/03a43b33f50475a632a91ea8c127f7e35e53786dbe6781c25f19fd5a65f8/coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3", size = 244000 }, - { url = "https://files.pythonhosted.org/packages/6a/89/ab6c43b1788a3128e4d1b7b54214548dcad75a621f9d277b14d16a80d8a1/coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d", size = 214195 }, - { url = "https://files.pythonhosted.org/packages/12/12/6bf5f9a8b063d116bac536a7fb594fc35cb04981654cccb4bbfea5dcdfa0/coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487", size = 214998 }, - { url = "https://files.pythonhosted.org/packages/2a/e6/1e9df74ef7a1c983a9c7443dac8aac37a46f1939ae3499424622e72a6f78/coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25", size = 212541 }, - { url = "https://files.pythonhosted.org/packages/04/51/c32174edb7ee49744e2e81c4b1414ac9df3dacfcb5b5f273b7f285ad43f6/coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42", size = 212767 }, - { url = "https://files.pythonhosted.org/packages/e9/8f/f454cbdb5212f13f29d4a7983db69169f1937e869a5142bce983ded52162/coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502", size = 256997 }, - { url = "https://files.pythonhosted.org/packages/e6/74/2bf9e78b321216d6ee90a81e5c22f912fc428442c830c4077b4a071db66f/coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1", size = 252708 }, - { url = "https://files.pythonhosted.org/packages/92/4d/50d7eb1e9a6062bee6e2f92e78b0998848a972e9afad349b6cdde6fa9e32/coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4", size = 255046 }, - { url = "https://files.pythonhosted.org/packages/40/9e/71fb4e7402a07c4198ab44fc564d09d7d0ffca46a9fb7b0a7b929e7641bd/coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73", size = 256139 }, - { url = "https://files.pythonhosted.org/packages/49/1a/78d37f7a42b5beff027e807c2843185961fdae7fe23aad5a4837c93f9d25/coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a", size = 254307 }, - { url = "https://files.pythonhosted.org/packages/58/e9/8fb8e0ff6bef5e170ee19d59ca694f9001b2ec085dc99b4f65c128bb3f9a/coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883", size = 255116 }, - { url = "https://files.pythonhosted.org/packages/56/b0/d968ecdbe6fe0a863de7169bbe9e8a476868959f3af24981f6a10d2b6924/coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada", size = 214909 }, - { url = "https://files.pythonhosted.org/packages/87/e9/d6b7ef9fecf42dfb418d93544af47c940aa83056c49e6021a564aafbc91f/coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257", size = 216068 }, - { url = "https://files.pythonhosted.org/packages/c4/f1/1da77bb4c920aa30e82fa9b6ea065da3467977c2e5e032e38e66f1c57ffd/coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd", size = 203443 }, - { url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435 }, +sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872, upload-time = "2025-03-30T20:36:45.376Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/77/074d201adb8383addae5784cb8e2dac60bb62bfdf28b2b10f3a3af2fda47/coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27", size = 211493, upload-time = "2025-03-30T20:35:12.286Z" }, + { url = "https://files.pythonhosted.org/packages/a9/89/7a8efe585750fe59b48d09f871f0e0c028a7b10722b2172dfe021fa2fdd4/coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea", size = 211921, upload-time = "2025-03-30T20:35:14.18Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ef/96a90c31d08a3f40c49dbe897df4f1fd51fb6583821a1a1c5ee30cc8f680/coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7", size = 244556, upload-time = "2025-03-30T20:35:15.616Z" }, + { url = "https://files.pythonhosted.org/packages/89/97/dcd5c2ce72cee9d7b0ee8c89162c24972fb987a111b92d1a3d1d19100c61/coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040", size = 242245, upload-time = "2025-03-30T20:35:18.648Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7b/b63cbb44096141ed435843bbb251558c8e05cc835c8da31ca6ffb26d44c0/coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543", size = 244032, upload-time = "2025-03-30T20:35:20.131Z" }, + { url = "https://files.pythonhosted.org/packages/97/e3/7fa8c2c00a1ef530c2a42fa5df25a6971391f92739d83d67a4ee6dcf7a02/coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2", size = 243679, upload-time = "2025-03-30T20:35:21.636Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b3/e0a59d8df9150c8a0c0841d55d6568f0a9195692136c44f3d21f1842c8f6/coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318", size = 241852, upload-time = "2025-03-30T20:35:23.525Z" }, + { url = "https://files.pythonhosted.org/packages/9b/82/db347ccd57bcef150c173df2ade97976a8367a3be7160e303e43dd0c795f/coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9", size = 242389, upload-time = "2025-03-30T20:35:25.09Z" }, + { url = "https://files.pythonhosted.org/packages/21/f6/3f7d7879ceb03923195d9ff294456241ed05815281f5254bc16ef71d6a20/coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c", size = 213997, upload-time = "2025-03-30T20:35:26.914Z" }, + { url = "https://files.pythonhosted.org/packages/28/87/021189643e18ecf045dbe1e2071b2747901f229df302de01c998eeadf146/coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78", size = 214911, upload-time = "2025-03-30T20:35:28.498Z" }, + { url = "https://files.pythonhosted.org/packages/aa/12/4792669473297f7973518bec373a955e267deb4339286f882439b8535b39/coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc", size = 211684, upload-time = "2025-03-30T20:35:29.959Z" }, + { url = "https://files.pythonhosted.org/packages/be/e1/2a4ec273894000ebedd789e8f2fc3813fcaf486074f87fd1c5b2cb1c0a2b/coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6", size = 211935, upload-time = "2025-03-30T20:35:31.912Z" }, + { url = "https://files.pythonhosted.org/packages/f8/3a/7b14f6e4372786709a361729164125f6b7caf4024ce02e596c4a69bccb89/coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d", size = 245994, upload-time = "2025-03-30T20:35:33.455Z" }, + { url = "https://files.pythonhosted.org/packages/54/80/039cc7f1f81dcbd01ea796d36d3797e60c106077e31fd1f526b85337d6a1/coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05", size = 242885, upload-time = "2025-03-30T20:35:35.354Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/dc8355f992b6cc2f9dcd5ef6242b62a3f73264893bc09fbb08bfcab18eb4/coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a", size = 245142, upload-time = "2025-03-30T20:35:37.121Z" }, + { url = "https://files.pythonhosted.org/packages/43/1b/33e313b22cf50f652becb94c6e7dae25d8f02e52e44db37a82de9ac357e8/coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6", size = 244906, upload-time = "2025-03-30T20:35:39.07Z" }, + { url = "https://files.pythonhosted.org/packages/05/08/c0a8048e942e7f918764ccc99503e2bccffba1c42568693ce6955860365e/coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47", size = 243124, upload-time = "2025-03-30T20:35:40.598Z" }, + { url = "https://files.pythonhosted.org/packages/5b/62/ea625b30623083c2aad645c9a6288ad9fc83d570f9adb913a2abdba562dd/coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe", size = 244317, upload-time = "2025-03-30T20:35:42.204Z" }, + { url = "https://files.pythonhosted.org/packages/62/cb/3871f13ee1130a6c8f020e2f71d9ed269e1e2124aa3374d2180ee451cee9/coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545", size = 214170, upload-time = "2025-03-30T20:35:44.216Z" }, + { url = "https://files.pythonhosted.org/packages/88/26/69fe1193ab0bfa1eb7a7c0149a066123611baba029ebb448500abd8143f9/coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b", size = 214969, upload-time = "2025-03-30T20:35:45.797Z" }, + { url = "https://files.pythonhosted.org/packages/f3/21/87e9b97b568e223f3438d93072479c2f36cc9b3f6b9f7094b9d50232acc0/coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd", size = 211708, upload-time = "2025-03-30T20:35:47.417Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/882d08b28a0d19c9c4c2e8a1c6ebe1f79c9c839eb46d4fca3bd3b34562b9/coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00", size = 211981, upload-time = "2025-03-30T20:35:49.002Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/ce99612ebd58082fbe3f8c66f6d8d5694976c76a0d474503fa70633ec77f/coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64", size = 245495, upload-time = "2025-03-30T20:35:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8d/6115abe97df98db6b2bd76aae395fcc941d039a7acd25f741312ced9a78f/coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067", size = 242538, upload-time = "2025-03-30T20:35:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008", size = 244561, upload-time = "2025-03-30T20:35:54.658Z" }, + { url = "https://files.pythonhosted.org/packages/22/70/c10c77cd77970ac965734fe3419f2c98665f6e982744a9bfb0e749d298f4/coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733", size = 244633, upload-time = "2025-03-30T20:35:56.221Z" }, + { url = "https://files.pythonhosted.org/packages/38/5a/4f7569d946a07c952688debee18c2bb9ab24f88027e3d71fd25dbc2f9dca/coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323", size = 242712, upload-time = "2025-03-30T20:35:57.801Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a1/03a43b33f50475a632a91ea8c127f7e35e53786dbe6781c25f19fd5a65f8/coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3", size = 244000, upload-time = "2025-03-30T20:35:59.378Z" }, + { url = "https://files.pythonhosted.org/packages/6a/89/ab6c43b1788a3128e4d1b7b54214548dcad75a621f9d277b14d16a80d8a1/coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d", size = 214195, upload-time = "2025-03-30T20:36:01.005Z" }, + { url = "https://files.pythonhosted.org/packages/12/12/6bf5f9a8b063d116bac536a7fb594fc35cb04981654cccb4bbfea5dcdfa0/coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487", size = 214998, upload-time = "2025-03-30T20:36:03.006Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e6/1e9df74ef7a1c983a9c7443dac8aac37a46f1939ae3499424622e72a6f78/coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25", size = 212541, upload-time = "2025-03-30T20:36:04.638Z" }, + { url = "https://files.pythonhosted.org/packages/04/51/c32174edb7ee49744e2e81c4b1414ac9df3dacfcb5b5f273b7f285ad43f6/coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42", size = 212767, upload-time = "2025-03-30T20:36:06.503Z" }, + { url = "https://files.pythonhosted.org/packages/e9/8f/f454cbdb5212f13f29d4a7983db69169f1937e869a5142bce983ded52162/coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502", size = 256997, upload-time = "2025-03-30T20:36:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/e6/74/2bf9e78b321216d6ee90a81e5c22f912fc428442c830c4077b4a071db66f/coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1", size = 252708, upload-time = "2025-03-30T20:36:09.781Z" }, + { url = "https://files.pythonhosted.org/packages/92/4d/50d7eb1e9a6062bee6e2f92e78b0998848a972e9afad349b6cdde6fa9e32/coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4", size = 255046, upload-time = "2025-03-30T20:36:11.409Z" }, + { url = "https://files.pythonhosted.org/packages/40/9e/71fb4e7402a07c4198ab44fc564d09d7d0ffca46a9fb7b0a7b929e7641bd/coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73", size = 256139, upload-time = "2025-03-30T20:36:13.86Z" }, + { url = "https://files.pythonhosted.org/packages/49/1a/78d37f7a42b5beff027e807c2843185961fdae7fe23aad5a4837c93f9d25/coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a", size = 254307, upload-time = "2025-03-30T20:36:16.074Z" }, + { url = "https://files.pythonhosted.org/packages/58/e9/8fb8e0ff6bef5e170ee19d59ca694f9001b2ec085dc99b4f65c128bb3f9a/coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883", size = 255116, upload-time = "2025-03-30T20:36:18.033Z" }, + { url = "https://files.pythonhosted.org/packages/56/b0/d968ecdbe6fe0a863de7169bbe9e8a476868959f3af24981f6a10d2b6924/coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada", size = 214909, upload-time = "2025-03-30T20:36:19.644Z" }, + { url = "https://files.pythonhosted.org/packages/87/e9/d6b7ef9fecf42dfb418d93544af47c940aa83056c49e6021a564aafbc91f/coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257", size = 216068, upload-time = "2025-03-30T20:36:21.282Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f1/1da77bb4c920aa30e82fa9b6ea065da3467977c2e5e032e38e66f1c57ffd/coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd", size = 203443, upload-time = "2025-03-30T20:36:41.959Z" }, + { url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435, upload-time = "2025-03-30T20:36:43.61Z" }, ] [package.optional-dependencies] @@ -164,9 +164,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "python-dateutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943 } +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034 }, + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, ] [[package]] @@ -176,9 +176,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "smmap" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684 } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794 }, + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, ] [[package]] @@ -188,9 +188,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/89/37df0b71473153574a5cdef8f242de422a0f5d26d7a9e231e6f169b4ad14/gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269", size = 214196 } +sdist = { url = "https://files.pythonhosted.org/packages/c0/89/37df0b71473153574a5cdef8f242de422a0f5d26d7a9e231e6f169b4ad14/gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269", size = 214196, upload-time = "2025-01-02T07:32:43.59Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599 }, + { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599, upload-time = "2025-01-02T07:32:40.731Z" }, ] [[package]] @@ -200,27 +200,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6a/ba/1ebe51a22c491a3fc94b44ef9c46a5b5472540e24a5c3f251cebbab7214b/griffe-1.6.1.tar.gz", hash = "sha256:ff0acf706b2680f8c721412623091c891e752b2c61b7037618f7b77d06732cf5", size = 393112 } +sdist = { url = "https://files.pythonhosted.org/packages/6a/ba/1ebe51a22c491a3fc94b44ef9c46a5b5472540e24a5c3f251cebbab7214b/griffe-1.6.1.tar.gz", hash = "sha256:ff0acf706b2680f8c721412623091c891e752b2c61b7037618f7b77d06732cf5", size = 393112, upload-time = "2025-03-18T15:18:45.489Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/d3/a760d1062e44587230aa65573c70edaad4ee8a0e60e193a3172b304d24d8/griffe-1.6.1-py3-none-any.whl", hash = "sha256:b0131670db16834f82383bcf4f788778853c9bf4dc7a1a2b708bb0808ca56a98", size = 128615 }, + { url = "https://files.pythonhosted.org/packages/1f/d3/a760d1062e44587230aa65573c70edaad4ee8a0e60e193a3172b304d24d8/griffe-1.6.1-py3-none-any.whl", hash = "sha256:b0131670db16834f82383bcf4f788778853c9bf4dc7a1a2b708bb0808ca56a98", size = 128615, upload-time = "2025-03-18T15:18:43.57Z" }, ] [[package]] name = "idna" version = "3.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] [[package]] name = "iniconfig" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" }, ] [[package]] @@ -230,75 +230,75 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] [[package]] name = "markdown" version = "3.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086 } +sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086, upload-time = "2024-08-16T15:55:17.812Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349 }, + { url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349, upload-time = "2024-08-16T15:55:16.176Z" }, ] [[package]] name = "markupsafe" version = "3.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, - { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, - { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, - { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, - { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, - { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, - { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, - { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, - { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, - { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, - { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, - { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, - { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, - { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, - { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, - { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, - { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, - { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, - { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, - { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, - { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, - { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, - { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, - { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, - { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, - { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, - { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, - { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, - { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, - { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, - { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, - { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, - { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, - { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, - { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, - { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, - { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, - { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, - { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, - { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, ] [[package]] name = "mergedeep" version = "1.3.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661 } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354 }, + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, ] [[package]] @@ -320,9 +320,9 @@ dependencies = [ { name = "pyyaml-env-tag" }, { name = "watchdog" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159 } +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451 }, + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, ] [[package]] @@ -334,9 +334,9 @@ dependencies = [ { name = "markupsafe" }, { name = "mkdocs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/44/140469d87379c02f1e1870315f3143718036a983dd0416650827b8883192/mkdocs_autorefs-1.4.1.tar.gz", hash = "sha256:4b5b6235a4becb2b10425c2fa191737e415b37aa3418919db33e5d774c9db079", size = 4131355 } +sdist = { url = "https://files.pythonhosted.org/packages/c2/44/140469d87379c02f1e1870315f3143718036a983dd0416650827b8883192/mkdocs_autorefs-1.4.1.tar.gz", hash = "sha256:4b5b6235a4becb2b10425c2fa191737e415b37aa3418919db33e5d774c9db079", size = 4131355, upload-time = "2025-03-08T13:35:21.232Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/29/1125f7b11db63e8e32bcfa0752a4eea30abff3ebd0796f808e14571ddaa2/mkdocs_autorefs-1.4.1-py3-none-any.whl", hash = "sha256:9793c5ac06a6ebbe52ec0f8439256e66187badf4b5334b5fde0b128ec134df4f", size = 5782047 }, + { url = "https://files.pythonhosted.org/packages/f8/29/1125f7b11db63e8e32bcfa0752a4eea30abff3ebd0796f808e14571ddaa2/mkdocs_autorefs-1.4.1-py3-none-any.whl", hash = "sha256:9793c5ac06a6ebbe52ec0f8439256e66187badf4b5334b5fde0b128ec134df4f", size = 5782047, upload-time = "2025-03-08T13:35:18.889Z" }, ] [[package]] @@ -347,9 +347,9 @@ dependencies = [ { name = "mkdocs" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1b/b5/f72df157abc7f85e33ffa417464e9dd535ef5fda7654eda41190047a53b6/mkdocs-codeinclude-plugin-0.2.1.tar.gz", hash = "sha256:305387f67a885f0e36ec1cf977324fe1fe50d31301147194b63631d0864601b1", size = 8140 } +sdist = { url = "https://files.pythonhosted.org/packages/1b/b5/f72df157abc7f85e33ffa417464e9dd535ef5fda7654eda41190047a53b6/mkdocs-codeinclude-plugin-0.2.1.tar.gz", hash = "sha256:305387f67a885f0e36ec1cf977324fe1fe50d31301147194b63631d0864601b1", size = 8140, upload-time = "2023-03-01T19:57:06.724Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/7b/60573ebf2a22b144eeaf3b29db9a6d4d342d68273f716ea2723d1ad723ba/mkdocs_codeinclude_plugin-0.2.1-py3-none-any.whl", hash = "sha256:172a917c9b257fa62850b669336151f85d3cd40312b2b52520cbcceab557ea6c", size = 8093 }, + { url = "https://files.pythonhosted.org/packages/4d/7b/60573ebf2a22b144eeaf3b29db9a6d4d342d68273f716ea2723d1ad723ba/mkdocs_codeinclude_plugin-0.2.1-py3-none-any.whl", hash = "sha256:172a917c9b257fa62850b669336151f85d3cd40312b2b52520cbcceab557ea6c", size = 8093, upload-time = "2023-03-01T19:57:05.207Z" }, ] [[package]] @@ -361,9 +361,9 @@ dependencies = [ { name = "platformdirs" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239 } +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521 }, + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, ] [[package]] @@ -373,9 +373,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mkdocs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/87/9a/063c4a3688e4669eb2054e4bf6e9cc582f6c1d85674e3f5b836ceff97c3b/mkdocs_git_authors_plugin-0.9.4.tar.gz", hash = "sha256:f5cfaf93d08981ce25591bbaf642051ed168c3886bb96ecd2dca53f0ef1973b8", size = 21914 } +sdist = { url = "https://files.pythonhosted.org/packages/87/9a/063c4a3688e4669eb2054e4bf6e9cc582f6c1d85674e3f5b836ceff97c3b/mkdocs_git_authors_plugin-0.9.4.tar.gz", hash = "sha256:f5cfaf93d08981ce25591bbaf642051ed168c3886bb96ecd2dca53f0ef1973b8", size = 21914, upload-time = "2025-03-14T19:26:40.873Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/ac/2b5bae4047276fda2bdd14a6d4af59288fb4d5de54151ae4e6ba17611ceb/mkdocs_git_authors_plugin-0.9.4-py3-none-any.whl", hash = "sha256:84b9b56c703841189c64d8ff6947034fe0a9c14a0a8f1f6255edfcfe3a56825f", size = 20752 }, + { url = "https://files.pythonhosted.org/packages/5c/ac/2b5bae4047276fda2bdd14a6d4af59288fb4d5de54151ae4e6ba17611ceb/mkdocs_git_authors_plugin-0.9.4-py3-none-any.whl", hash = "sha256:84b9b56c703841189c64d8ff6947034fe0a9c14a0a8f1f6255edfcfe3a56825f", size = 20752, upload-time = "2025-03-14T19:26:39.398Z" }, ] [[package]] @@ -387,9 +387,9 @@ dependencies = [ { name = "mkdocs" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b4/8a/4ca4fb7d17f66fa709b49744c597204ad03fb3b011c76919564843426f11/mkdocs_git_committers_plugin_2-2.5.0.tar.gz", hash = "sha256:a01f17369e79ca28651681cddf212770e646e6191954bad884ca3067316aae60", size = 15183 } +sdist = { url = "https://files.pythonhosted.org/packages/b4/8a/4ca4fb7d17f66fa709b49744c597204ad03fb3b011c76919564843426f11/mkdocs_git_committers_plugin_2-2.5.0.tar.gz", hash = "sha256:a01f17369e79ca28651681cddf212770e646e6191954bad884ca3067316aae60", size = 15183, upload-time = "2025-01-30T07:30:48.667Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/f5/768590251839a148c188d64779b809bde0e78a306295c18fc29d7fc71ce1/mkdocs_git_committers_plugin_2-2.5.0-py3-none-any.whl", hash = "sha256:1778becf98ccdc5fac809ac7b62cf01d3c67d6e8432723dffbb823307d1193c4", size = 11788 }, + { url = "https://files.pythonhosted.org/packages/8e/f5/768590251839a148c188d64779b809bde0e78a306295c18fc29d7fc71ce1/mkdocs_git_committers_plugin_2-2.5.0-py3-none-any.whl", hash = "sha256:1778becf98ccdc5fac809ac7b62cf01d3c67d6e8432723dffbb823307d1193c4", size = 11788, upload-time = "2025-01-30T07:30:45.748Z" }, ] [[package]] @@ -409,18 +409,31 @@ dependencies = [ { name = "pymdown-extensions" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/11/cb/6dd3b6a7925429c0229738098ee874dbf7fa02db55558adb2c5bf86077b2/mkdocs_material-9.6.9.tar.gz", hash = "sha256:a4872139715a1f27b2aa3f3dc31a9794b7bbf36333c0ba4607cf04786c94f89c", size = 3948083 } +sdist = { url = "https://files.pythonhosted.org/packages/11/cb/6dd3b6a7925429c0229738098ee874dbf7fa02db55558adb2c5bf86077b2/mkdocs_material-9.6.9.tar.gz", hash = "sha256:a4872139715a1f27b2aa3f3dc31a9794b7bbf36333c0ba4607cf04786c94f89c", size = 3948083, upload-time = "2025-03-17T09:28:50.879Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/7c/ea5a671b2ff5d0e3f3108a7f7d75b541d683e4969aaead2a8f3e59e0fc27/mkdocs_material-9.6.9-py3-none-any.whl", hash = "sha256:6e61b7fb623ce2aa4622056592b155a9eea56ff3487d0835075360be45a4c8d1", size = 8697935 }, + { url = "https://files.pythonhosted.org/packages/db/7c/ea5a671b2ff5d0e3f3108a7f7d75b541d683e4969aaead2a8f3e59e0fc27/mkdocs_material-9.6.9-py3-none-any.whl", hash = "sha256:6e61b7fb623ce2aa4622056592b155a9eea56ff3487d0835075360be45a4c8d1", size = 8697935, upload-time = "2025-03-17T09:28:47.481Z" }, ] [[package]] name = "mkdocs-material-extensions" version = "1.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847 } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728 }, + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "mkdocs-monorepo-plugin" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mkdocs" }, + { name = "python-slugify" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/6c/5b2a34fd63fe20724e2edf1879e977b40453efe40e1c385a05f38b420664/mkdocs-monorepo-plugin-1.1.0.tar.gz", hash = "sha256:ccc566e166aac5ae7fade498c15c4a337a4892d238629b51aba8ef3fc7099034", size = 13435, upload-time = "2024-01-04T14:29:15.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/33/4cc6c70223aee511244f8fe7706df70d1cd253d1446ab466c73f9dfbaab5/mkdocs_monorepo_plugin-1.1.0-py3-none-any.whl", hash = "sha256:7bbfd9756a7fdecf64d6105dad96cce7e7bb5f0d6cfc2bfda31a1919c77cc3b9", size = 14312, upload-time = "2024-01-04T14:29:14.09Z" }, ] [[package]] @@ -435,9 +448,9 @@ dependencies = [ { name = "mkdocs-autorefs" }, { name = "pymdown-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8e/4d/a9484dc5d926295bdf308f1f6c4f07fcc99735b970591edc414d401fcc91/mkdocstrings-0.29.0.tar.gz", hash = "sha256:3657be1384543ce0ee82112c3e521bbf48e41303aa0c229b9ffcccba057d922e", size = 1212185 } +sdist = { url = "https://files.pythonhosted.org/packages/8e/4d/a9484dc5d926295bdf308f1f6c4f07fcc99735b970591edc414d401fcc91/mkdocstrings-0.29.0.tar.gz", hash = "sha256:3657be1384543ce0ee82112c3e521bbf48e41303aa0c229b9ffcccba057d922e", size = 1212185, upload-time = "2025-03-10T13:10:11.445Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/47/eb876dfd84e48f31ff60897d161b309cf6a04ca270155b0662aae562b3fb/mkdocstrings-0.29.0-py3-none-any.whl", hash = "sha256:8ea98358d2006f60befa940fdebbbc88a26b37ecbcded10be726ba359284f73d", size = 1630824 }, + { url = "https://files.pythonhosted.org/packages/15/47/eb876dfd84e48f31ff60897d161b309cf6a04ca270155b0662aae562b3fb/mkdocstrings-0.29.0-py3-none-any.whl", hash = "sha256:8ea98358d2006f60befa940fdebbbc88a26b37ecbcded10be726ba359284f73d", size = 1630824, upload-time = "2025-03-10T13:10:09.712Z" }, ] [[package]] @@ -449,63 +462,63 @@ dependencies = [ { name = "mkdocs-autorefs" }, { name = "mkdocstrings" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8e/e7/0691e34e807a8f5c28f0988fcfeeb584f0b569ce433bf341944f14bdb3ff/mkdocstrings_python-1.16.6.tar.gz", hash = "sha256:cefe0f0e17ab4a4611f01b0a2af75e4298664e0ff54feb83c91a485bfed82dc9", size = 201565 } +sdist = { url = "https://files.pythonhosted.org/packages/8e/e7/0691e34e807a8f5c28f0988fcfeeb584f0b569ce433bf341944f14bdb3ff/mkdocstrings_python-1.16.6.tar.gz", hash = "sha256:cefe0f0e17ab4a4611f01b0a2af75e4298664e0ff54feb83c91a485bfed82dc9", size = 201565, upload-time = "2025-03-18T15:34:24.371Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/42/ed682687ef5f248e104f82806d5d9893f6dd81d8cb4561692e190ba1a252/mkdocstrings_python-1.16.6-py3-none-any.whl", hash = "sha256:de877dd71f69878c973c4897a39683b7b6961bee7b058879095b69681488453f", size = 123207 }, + { url = "https://files.pythonhosted.org/packages/6a/42/ed682687ef5f248e104f82806d5d9893f6dd81d8cb4561692e190ba1a252/mkdocstrings_python-1.16.6-py3-none-any.whl", hash = "sha256:de877dd71f69878c973c4897a39683b7b6961bee7b058879095b69681488453f", size = 123207, upload-time = "2025-03-18T15:34:21.357Z" }, ] [[package]] name = "packaging" version = "24.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950, upload-time = "2024-11-08T09:47:47.202Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload-time = "2024-11-08T09:47:44.722Z" }, ] [[package]] name = "paginate" version = "0.5.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252 } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746 }, + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, ] [[package]] name = "pathspec" version = "0.12.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, ] [[package]] name = "platformdirs" version = "4.3.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302, upload-time = "2024-09-17T19:06:50.688Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439, upload-time = "2024-09-17T19:06:49.212Z" }, ] [[package]] name = "pluggy" version = "1.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, ] [[package]] name = "pygments" version = "2.19.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, ] [[package]] @@ -523,6 +536,7 @@ dev = [ { name = "mkdocs-git-authors-plugin" }, { name = "mkdocs-git-committers-plugin-2" }, { name = "mkdocs-material" }, + { name = "mkdocs-monorepo-plugin" }, { name = "mkdocstrings-python" }, { name = "pytest" }, { name = "pytest-cov" }, @@ -539,6 +553,7 @@ dev = [ { name = "mkdocs-git-authors-plugin", specifier = ">=0.9.4" }, { name = "mkdocs-git-committers-plugin-2", specifier = ">=2.5.0" }, { name = "mkdocs-material", specifier = ">=9.6.9" }, + { name = "mkdocs-monorepo-plugin", specifier = ">=1.1.0" }, { name = "mkdocstrings-python", specifier = ">=1.16.6" }, { name = "pytest", specifier = ">=8.3.5" }, { name = "pytest-cov", specifier = ">=6.1.1" }, @@ -553,18 +568,18 @@ dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7c/44/e6de2fdc880ad0ec7547ca2e087212be815efbc9a425a8d5ba9ede602cbb/pymdown_extensions-10.14.3.tar.gz", hash = "sha256:41e576ce3f5d650be59e900e4ceff231e0aed2a88cf30acaee41e02f063a061b", size = 846846 } +sdist = { url = "https://files.pythonhosted.org/packages/7c/44/e6de2fdc880ad0ec7547ca2e087212be815efbc9a425a8d5ba9ede602cbb/pymdown_extensions-10.14.3.tar.gz", hash = "sha256:41e576ce3f5d650be59e900e4ceff231e0aed2a88cf30acaee41e02f063a061b", size = 846846, upload-time = "2025-02-01T15:43:15.42Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/f5/b9e2a42aa8f9e34d52d66de87941ecd236570c7ed2e87775ed23bbe4e224/pymdown_extensions-10.14.3-py3-none-any.whl", hash = "sha256:05e0bee73d64b9c71a4ae17c72abc2f700e8bc8403755a00580b49a4e9f189e9", size = 264467 }, + { url = "https://files.pythonhosted.org/packages/eb/f5/b9e2a42aa8f9e34d52d66de87941ecd236570c7ed2e87775ed23bbe4e224/pymdown_extensions-10.14.3-py3-none-any.whl", hash = "sha256:05e0bee73d64b9c71a4ae17c72abc2f700e8bc8403755a00580b49a4e9f189e9", size = 264467, upload-time = "2025-02-01T15:43:13.995Z" }, ] [[package]] name = "pyserial" version = "3.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1e/7d/ae3f0a63f41e4d2f6cb66a5b57197850f919f59e558159a4dd3a818f5082/pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb", size = 159125 } +sdist = { url = "https://files.pythonhosted.org/packages/1e/7d/ae3f0a63f41e4d2f6cb66a5b57197850f919f59e558159a4dd3a818f5082/pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb", size = 159125, upload-time = "2020-11-23T03:59:15.045Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0", size = 90585 }, + { url = "https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0", size = 90585, upload-time = "2020-11-23T03:59:13.41Z" }, ] [[package]] @@ -577,9 +592,9 @@ dependencies = [ { name = "packaging" }, { name = "pluggy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, ] [[package]] @@ -590,9 +605,9 @@ dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857 } +sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857, upload-time = "2025-04-05T14:07:51.592Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841 }, + { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841, upload-time = "2025-04-05T14:07:49.641Z" }, ] [[package]] @@ -602,44 +617,56 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-slugify" +version = "8.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "text-unidecode" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/c7/5e1547c44e31da50a460df93af11a535ace568ef89d7a811069ead340c4a/python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856", size = 10921, upload-time = "2024-02-08T18:32:45.488Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, + { url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051, upload-time = "2024-02-08T18:32:43.911Z" }, ] [[package]] name = "pyyaml" version = "6.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, - { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, - { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, - { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, - { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, - { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, - { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, - { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, - { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, - { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, - { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, - { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, - { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, - { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, - { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, - { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, ] [[package]] @@ -649,9 +676,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/8e/da1c6c58f751b70f8ceb1eb25bc25d524e8f14fe16edcce3f4e3ba08629c/pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb", size = 5631 } +sdist = { url = "https://files.pythonhosted.org/packages/fb/8e/da1c6c58f751b70f8ceb1eb25bc25d524e8f14fe16edcce3f4e3ba08629c/pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb", size = 5631, upload-time = "2020-11-12T02:38:26.239Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/66/bbb1dd374f5c870f59c5bb1db0e18cbe7fa739415a24cbd95b2d1f5ae0c4/pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069", size = 3911 }, + { url = "https://files.pythonhosted.org/packages/5a/66/bbb1dd374f5c870f59c5bb1db0e18cbe7fa739415a24cbd95b2d1f5ae0c4/pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069", size = 3911, upload-time = "2020-11-12T02:38:24.638Z" }, ] [[package]] @@ -664,125 +691,134 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, ] [[package]] name = "ruff" version = "0.11.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/77/2b/7ca27e854d92df5e681e6527dc0f9254c9dc06c8408317893cf96c851cdd/ruff-0.11.0.tar.gz", hash = "sha256:e55c620690a4a7ee6f1cccb256ec2157dc597d109400ae75bbf944fc9d6462e2", size = 3799407 } +sdist = { url = "https://files.pythonhosted.org/packages/77/2b/7ca27e854d92df5e681e6527dc0f9254c9dc06c8408317893cf96c851cdd/ruff-0.11.0.tar.gz", hash = "sha256:e55c620690a4a7ee6f1cccb256ec2157dc597d109400ae75bbf944fc9d6462e2", size = 3799407, upload-time = "2025-03-14T13:52:36.539Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/40/3d0340a9e5edc77d37852c0cd98c5985a5a8081fc3befaeb2ae90aaafd2b/ruff-0.11.0-py3-none-linux_armv6l.whl", hash = "sha256:dc67e32bc3b29557513eb7eeabb23efdb25753684b913bebb8a0c62495095acb", size = 10098158 }, - { url = "https://files.pythonhosted.org/packages/ec/a9/d8f5abb3b87b973b007649ac7bf63665a05b2ae2b2af39217b09f52abbbf/ruff-0.11.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38c23fd9bdec4eb437b4c1e3595905a0a8edfccd63a790f818b28c78fe345639", size = 10879071 }, - { url = "https://files.pythonhosted.org/packages/ab/62/aaa198614c6211677913ec480415c5e6509586d7b796356cec73a2f8a3e6/ruff-0.11.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7c8661b0be91a38bd56db593e9331beaf9064a79028adee2d5f392674bbc5e88", size = 10247944 }, - { url = "https://files.pythonhosted.org/packages/9f/52/59e0a9f2cf1ce5e6cbe336b6dd0144725c8ea3b97cac60688f4e7880bf13/ruff-0.11.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6c0e8d3d2db7e9f6efd884f44b8dc542d5b6b590fc4bb334fdbc624d93a29a2", size = 10421725 }, - { url = "https://files.pythonhosted.org/packages/a6/c3/dcd71acc6dff72ce66d13f4be5bca1dbed4db678dff2f0f6f307b04e5c02/ruff-0.11.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c3156d3f4b42e57247275a0a7e15a851c165a4fc89c5e8fa30ea6da4f7407b8", size = 9954435 }, - { url = "https://files.pythonhosted.org/packages/a6/9a/342d336c7c52dbd136dee97d4c7797e66c3f92df804f8f3b30da59b92e9c/ruff-0.11.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:490b1e147c1260545f6d041c4092483e3f6d8eba81dc2875eaebcf9140b53905", size = 11492664 }, - { url = "https://files.pythonhosted.org/packages/84/35/6e7defd2d7ca95cc385ac1bd9f7f2e4a61b9cc35d60a263aebc8e590c462/ruff-0.11.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1bc09a7419e09662983b1312f6fa5dab829d6ab5d11f18c3760be7ca521c9329", size = 12207856 }, - { url = "https://files.pythonhosted.org/packages/22/78/da669c8731bacf40001c880ada6d31bcfb81f89cc996230c3b80d319993e/ruff-0.11.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcfa478daf61ac8002214eb2ca5f3e9365048506a9d52b11bea3ecea822bb844", size = 11645156 }, - { url = "https://files.pythonhosted.org/packages/ee/47/e27d17d83530a208f4a9ab2e94f758574a04c51e492aa58f91a3ed7cbbcb/ruff-0.11.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6fbb2aed66fe742a6a3a0075ed467a459b7cedc5ae01008340075909d819df1e", size = 13884167 }, - { url = "https://files.pythonhosted.org/packages/9f/5e/42ffbb0a5d4b07bbc642b7d58357b4e19a0f4774275ca6ca7d1f7b5452cd/ruff-0.11.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92c0c1ff014351c0b0cdfdb1e35fa83b780f1e065667167bb9502d47ca41e6db", size = 11348311 }, - { url = "https://files.pythonhosted.org/packages/c8/51/dc3ce0c5ce1a586727a3444a32f98b83ba99599bb1ebca29d9302886e87f/ruff-0.11.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e4fd5ff5de5f83e0458a138e8a869c7c5e907541aec32b707f57cf9a5e124445", size = 10305039 }, - { url = "https://files.pythonhosted.org/packages/60/e0/475f0c2f26280f46f2d6d1df1ba96b3399e0234cf368cc4c88e6ad10dcd9/ruff-0.11.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:96bc89a5c5fd21a04939773f9e0e276308be0935de06845110f43fd5c2e4ead7", size = 9937939 }, - { url = "https://files.pythonhosted.org/packages/e2/d3/3e61b7fd3e9cdd1e5b8c7ac188bec12975c824e51c5cd3d64caf81b0331e/ruff-0.11.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a9352b9d767889ec5df1483f94870564e8102d4d7e99da52ebf564b882cdc2c7", size = 10923259 }, - { url = "https://files.pythonhosted.org/packages/30/32/cd74149ebb40b62ddd14bd2d1842149aeb7f74191fb0f49bd45c76909ff2/ruff-0.11.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:049a191969a10897fe052ef9cc7491b3ef6de79acd7790af7d7897b7a9bfbcb6", size = 11406212 }, - { url = "https://files.pythonhosted.org/packages/00/ef/033022a6b104be32e899b00de704d7c6d1723a54d4c9e09d147368f14b62/ruff-0.11.0-py3-none-win32.whl", hash = "sha256:3191e9116b6b5bbe187447656f0c8526f0d36b6fd89ad78ccaad6bdc2fad7df2", size = 10310905 }, - { url = "https://files.pythonhosted.org/packages/ed/8a/163f2e78c37757d035bd56cd60c8d96312904ca4a6deeab8442d7b3cbf89/ruff-0.11.0-py3-none-win_amd64.whl", hash = "sha256:c58bfa00e740ca0a6c43d41fb004cd22d165302f360aaa56f7126d544db31a21", size = 11411730 }, - { url = "https://files.pythonhosted.org/packages/4e/f7/096f6efabe69b49d7ca61052fc70289c05d8d35735c137ef5ba5ef423662/ruff-0.11.0-py3-none-win_arm64.whl", hash = "sha256:868364fc23f5aa122b00c6f794211e85f7e78f5dffdf7c590ab90b8c4e69b657", size = 10538956 }, + { url = "https://files.pythonhosted.org/packages/48/40/3d0340a9e5edc77d37852c0cd98c5985a5a8081fc3befaeb2ae90aaafd2b/ruff-0.11.0-py3-none-linux_armv6l.whl", hash = "sha256:dc67e32bc3b29557513eb7eeabb23efdb25753684b913bebb8a0c62495095acb", size = 10098158, upload-time = "2025-03-14T13:51:55.69Z" }, + { url = "https://files.pythonhosted.org/packages/ec/a9/d8f5abb3b87b973b007649ac7bf63665a05b2ae2b2af39217b09f52abbbf/ruff-0.11.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38c23fd9bdec4eb437b4c1e3595905a0a8edfccd63a790f818b28c78fe345639", size = 10879071, upload-time = "2025-03-14T13:51:58.989Z" }, + { url = "https://files.pythonhosted.org/packages/ab/62/aaa198614c6211677913ec480415c5e6509586d7b796356cec73a2f8a3e6/ruff-0.11.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7c8661b0be91a38bd56db593e9331beaf9064a79028adee2d5f392674bbc5e88", size = 10247944, upload-time = "2025-03-14T13:52:02.318Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/59e0a9f2cf1ce5e6cbe336b6dd0144725c8ea3b97cac60688f4e7880bf13/ruff-0.11.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6c0e8d3d2db7e9f6efd884f44b8dc542d5b6b590fc4bb334fdbc624d93a29a2", size = 10421725, upload-time = "2025-03-14T13:52:04.303Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c3/dcd71acc6dff72ce66d13f4be5bca1dbed4db678dff2f0f6f307b04e5c02/ruff-0.11.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c3156d3f4b42e57247275a0a7e15a851c165a4fc89c5e8fa30ea6da4f7407b8", size = 9954435, upload-time = "2025-03-14T13:52:06.602Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9a/342d336c7c52dbd136dee97d4c7797e66c3f92df804f8f3b30da59b92e9c/ruff-0.11.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:490b1e147c1260545f6d041c4092483e3f6d8eba81dc2875eaebcf9140b53905", size = 11492664, upload-time = "2025-03-14T13:52:08.613Z" }, + { url = "https://files.pythonhosted.org/packages/84/35/6e7defd2d7ca95cc385ac1bd9f7f2e4a61b9cc35d60a263aebc8e590c462/ruff-0.11.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1bc09a7419e09662983b1312f6fa5dab829d6ab5d11f18c3760be7ca521c9329", size = 12207856, upload-time = "2025-03-14T13:52:11.019Z" }, + { url = "https://files.pythonhosted.org/packages/22/78/da669c8731bacf40001c880ada6d31bcfb81f89cc996230c3b80d319993e/ruff-0.11.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcfa478daf61ac8002214eb2ca5f3e9365048506a9d52b11bea3ecea822bb844", size = 11645156, upload-time = "2025-03-14T13:52:13.383Z" }, + { url = "https://files.pythonhosted.org/packages/ee/47/e27d17d83530a208f4a9ab2e94f758574a04c51e492aa58f91a3ed7cbbcb/ruff-0.11.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6fbb2aed66fe742a6a3a0075ed467a459b7cedc5ae01008340075909d819df1e", size = 13884167, upload-time = "2025-03-14T13:52:15.528Z" }, + { url = "https://files.pythonhosted.org/packages/9f/5e/42ffbb0a5d4b07bbc642b7d58357b4e19a0f4774275ca6ca7d1f7b5452cd/ruff-0.11.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92c0c1ff014351c0b0cdfdb1e35fa83b780f1e065667167bb9502d47ca41e6db", size = 11348311, upload-time = "2025-03-14T13:52:18.088Z" }, + { url = "https://files.pythonhosted.org/packages/c8/51/dc3ce0c5ce1a586727a3444a32f98b83ba99599bb1ebca29d9302886e87f/ruff-0.11.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e4fd5ff5de5f83e0458a138e8a869c7c5e907541aec32b707f57cf9a5e124445", size = 10305039, upload-time = "2025-03-14T13:52:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/60/e0/475f0c2f26280f46f2d6d1df1ba96b3399e0234cf368cc4c88e6ad10dcd9/ruff-0.11.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:96bc89a5c5fd21a04939773f9e0e276308be0935de06845110f43fd5c2e4ead7", size = 9937939, upload-time = "2025-03-14T13:52:22.798Z" }, + { url = "https://files.pythonhosted.org/packages/e2/d3/3e61b7fd3e9cdd1e5b8c7ac188bec12975c824e51c5cd3d64caf81b0331e/ruff-0.11.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a9352b9d767889ec5df1483f94870564e8102d4d7e99da52ebf564b882cdc2c7", size = 10923259, upload-time = "2025-03-14T13:52:24.89Z" }, + { url = "https://files.pythonhosted.org/packages/30/32/cd74149ebb40b62ddd14bd2d1842149aeb7f74191fb0f49bd45c76909ff2/ruff-0.11.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:049a191969a10897fe052ef9cc7491b3ef6de79acd7790af7d7897b7a9bfbcb6", size = 11406212, upload-time = "2025-03-14T13:52:27.493Z" }, + { url = "https://files.pythonhosted.org/packages/00/ef/033022a6b104be32e899b00de704d7c6d1723a54d4c9e09d147368f14b62/ruff-0.11.0-py3-none-win32.whl", hash = "sha256:3191e9116b6b5bbe187447656f0c8526f0d36b6fd89ad78ccaad6bdc2fad7df2", size = 10310905, upload-time = "2025-03-14T13:52:30.46Z" }, + { url = "https://files.pythonhosted.org/packages/ed/8a/163f2e78c37757d035bd56cd60c8d96312904ca4a6deeab8442d7b3cbf89/ruff-0.11.0-py3-none-win_amd64.whl", hash = "sha256:c58bfa00e740ca0a6c43d41fb004cd22d165302f360aaa56f7126d544db31a21", size = 11411730, upload-time = "2025-03-14T13:52:32.508Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f7/096f6efabe69b49d7ca61052fc70289c05d8d35735c137ef5ba5ef423662/ruff-0.11.0-py3-none-win_arm64.whl", hash = "sha256:868364fc23f5aa122b00c6f794211e85f7e78f5dffdf7c590ab90b8c4e69b657", size = 10538956, upload-time = "2025-03-14T13:52:34.491Z" }, ] [[package]] name = "six" version = "1.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] name = "smmap" version = "5.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329 } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, +] + +[[package]] +name = "text-unidecode" +version = "1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93", size = 76885, upload-time = "2019-08-30T21:36:45.405Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303 }, + { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154, upload-time = "2019-08-30T21:37:03.543Z" }, ] [[package]] name = "tomli" version = "2.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, - { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, - { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, - { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, - { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, - { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, - { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, - { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, - { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, - { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, - { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, - { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, - { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, - { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, - { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, - { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, - { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, - { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, - { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, - { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, - { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, - { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, - { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, - { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, - { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, - { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, - { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, - { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, - { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, - { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, - { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, ] [[package]] name = "urllib3" version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } +sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268, upload-time = "2024-12-22T07:47:30.032Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, + { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369, upload-time = "2024-12-22T07:47:28.074Z" }, ] [[package]] name = "watchdog" version = "6.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393 }, - { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392 }, - { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019 }, - { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471 }, - { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449 }, - { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054 }, - { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480 }, - { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451 }, - { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057 }, - { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079 }, - { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078 }, - { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076 }, - { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077 }, - { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078 }, - { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077 }, - { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078 }, - { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065 }, - { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 }, - { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 }, +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, ] From 0f7bb9f28c9cce7dc0480f88d32095c7cd6f5787 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Tue, 20 May 2025 09:39:43 +0100 Subject: [PATCH 112/267] Fix typo --- pyharp/messages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyharp/messages.py b/pyharp/messages.py index e0bb046..c7df70b 100644 --- a/pyharp/messages.py +++ b/pyharp/messages.py @@ -350,7 +350,7 @@ def create( ) else: raise Exception( - "The value cannot be None is message type is equal to MessageType.WRITE!" + "The value cannot be None if the message type is equal to MessageType.WRITE!" ) def __repr__(self) -> str: From b977d7d3aa517a8105e0c08e167fe1efc760abad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Tue, 20 May 2025 14:11:45 +0100 Subject: [PATCH 113/267] Test new module structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- docs/examples/get_info/get_info.py | 2 +- .../olfactometer_example.py | 6 +- .../read_and_write_from_registers.py | 6 +- .../wait_for_events/wait_for_events.py | 4 +- pyharp/__init__.py | 3 - pyharp/devices/.gitignore | 0 pyharp/drivers/behavior.py | 6 +- pyharp/protocol/__init__.py | 9 +++ pyharp/{ => protocol}/base.py | 0 pyharp/{ => protocol}/device.py | 18 +++-- pyharp/{ => protocol}/device_names.py | 0 pyharp/protocol/exceptions.py | 4 + pyharp/{ => protocol}/harp_serial.py | 74 ++++++++++++++++--- pyharp/{ => protocol}/messages.py | 2 +- tests/test_messages.py | 4 +- 15 files changed, 106 insertions(+), 32 deletions(-) delete mode 100644 pyharp/__init__.py create mode 100644 pyharp/devices/.gitignore create mode 100644 pyharp/protocol/__init__.py rename pyharp/{ => protocol}/base.py (100%) rename pyharp/{ => protocol}/device.py (98%) rename pyharp/{ => protocol}/device_names.py (100%) create mode 100644 pyharp/protocol/exceptions.py rename pyharp/{ => protocol}/harp_serial.py (64%) rename pyharp/{ => protocol}/messages.py (99%) diff --git a/docs/examples/get_info/get_info.py b/docs/examples/get_info/get_info.py index 318c307..ea88143 100755 --- a/docs/examples/get_info/get_info.py +++ b/docs/examples/get_info/get_info.py @@ -1,4 +1,4 @@ -from pyharp.device import Device +from pyharp.protocol.device import Device SERIAL_PORT = ( "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port) diff --git a/docs/examples/olfactometer_example/olfactometer_example.py b/docs/examples/olfactometer_example/olfactometer_example.py index c4b2fe3..243e3c2 100644 --- a/docs/examples/olfactometer_example/olfactometer_example.py +++ b/docs/examples/olfactometer_example/olfactometer_example.py @@ -4,9 +4,9 @@ from serial import SerialException -from pyharp import MessageType, PayloadType -from pyharp.device import Device, OperationMode -from pyharp.messages import HarpMessage +from pyharp.protocol import MessageType, PayloadType +from pyharp.protocol.device import Device, OperationMode +from pyharp.protocol.messages import HarpMessage SERIAL_PORT = ( "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port) diff --git a/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py b/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py index cd8fab0..0858bb6 100755 --- a/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py +++ b/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py @@ -1,8 +1,8 @@ from serial import SerialException -from pyharp import MessageType, PayloadType -from pyharp.device import Device -from pyharp.messages import HarpMessage +from pyharp.protocol import MessageType, PayloadType +from pyharp.protocol.device import Device +from pyharp.protocol.messages import HarpMessage SERIAL_PORT = ( "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port) diff --git a/docs/examples/wait_for_events/wait_for_events.py b/docs/examples/wait_for_events/wait_for_events.py index e1064c2..a33c59a 100755 --- a/docs/examples/wait_for_events/wait_for_events.py +++ b/docs/examples/wait_for_events/wait_for_events.py @@ -1,5 +1,5 @@ -from pyharp import OperationMode -from pyharp.device import Device +from pyharp.protocol import OperationMode +from pyharp.protocol.device import Device SERIAL_PORT = ( "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port) diff --git a/pyharp/__init__.py b/pyharp/__init__.py deleted file mode 100644 index d40c35b..0000000 --- a/pyharp/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from pyharp.base import CommonRegisters, MessageType, OperationMode, PayloadType - -__version__ = "0.1.0" diff --git a/pyharp/devices/.gitignore b/pyharp/devices/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/pyharp/drivers/behavior.py b/pyharp/drivers/behavior.py index 04aa2d4..804b7c5 100644 --- a/pyharp/drivers/behavior.py +++ b/pyharp/drivers/behavior.py @@ -4,9 +4,9 @@ from serial.serialutil import SerialException -from pyharp.base import PayloadType -from pyharp.device import Device -from pyharp.messages import ReadHarpMessage, ReplyHarpMessage, WriteHarpMessage +from pyharp.protocol import PayloadType +from pyharp.protocol.device import Device +from pyharp.protocol.messages import ReadHarpMessage, ReplyHarpMessage, WriteHarpMessage # These definitions are from app_regs.h in the firmware. # Type, Base Address, "Description." diff --git a/pyharp/protocol/__init__.py b/pyharp/protocol/__init__.py new file mode 100644 index 0000000..2f73504 --- /dev/null +++ b/pyharp/protocol/__init__.py @@ -0,0 +1,9 @@ +from .base import ( + ClockConfig, + CommonRegisters, + MessageType, + OperationCtrl, + OperationMode, + PayloadType, + ResetMode, +) diff --git a/pyharp/base.py b/pyharp/protocol/base.py similarity index 100% rename from pyharp/base.py rename to pyharp/protocol/base.py diff --git a/pyharp/device.py b/pyharp/protocol/device.py similarity index 98% rename from pyharp/device.py rename to pyharp/protocol/device.py index ebc94ea..7125c73 100644 --- a/pyharp/device.py +++ b/pyharp/protocol/device.py @@ -8,11 +8,18 @@ import serial -from pyharp import CommonRegisters, MessageType, OperationMode, PayloadType -from pyharp.base import ClockConfig, OperationCtrl, ResetMode -from pyharp.device_names import device_names -from pyharp.harp_serial import HarpSerial -from pyharp.messages import HarpMessage, ReplyHarpMessage +from pyharp.protocol import ( + ClockConfig, + CommonRegisters, + MessageType, + OperationCtrl, + OperationMode, + PayloadType, + ResetMode, +) +from pyharp.protocol.device_names import device_names +from pyharp.protocol.harp_serial import HarpSerial +from pyharp.protocol.messages import HarpMessage, ReplyHarpMessage class Device: @@ -133,6 +140,7 @@ def connect(self) -> None: """ self._ser = HarpSerial( self._serial_port, # "/dev/tty.usbserial-A106C8O9" + use_buffered_protocol=False, baudrate=1000000, timeout=self._TIMEOUT_S, parity=serial.PARITY_NONE, diff --git a/pyharp/device_names.py b/pyharp/protocol/device_names.py similarity index 100% rename from pyharp/device_names.py rename to pyharp/protocol/device_names.py diff --git a/pyharp/protocol/exceptions.py b/pyharp/protocol/exceptions.py new file mode 100644 index 0000000..81cd8ae --- /dev/null +++ b/pyharp/protocol/exceptions.py @@ -0,0 +1,4 @@ +class HarpException(Exception): + """Base class for all exceptions raised by pyHARP.""" + + pass diff --git a/pyharp/harp_serial.py b/pyharp/protocol/harp_serial.py similarity index 64% rename from pyharp/harp_serial.py rename to pyharp/protocol/harp_serial.py index bed20b3..f264f34 100644 --- a/pyharp/harp_serial.py +++ b/pyharp/protocol/harp_serial.py @@ -7,7 +7,19 @@ import serial import serial.threaded -from pyharp.messages import HarpMessage, MessageType +from pyharp.protocol.messages import HarpMessage, MessageType + + +class HarpSerialProtocolOld(serial.threaded.Protocol): + # Old implementation (per-byte queue) + def __init__(self, read_q: queue.Queue, *args, **kwargs): + self._read_q = read_q + super().__init__(*args, **kwargs) + + def data_received(self, data: bytes) -> None: + for byte in data: + self._read_q.put(byte) + return super().data_received(data) class HarpSerialProtocol(serial.threaded.Protocol): @@ -25,6 +37,7 @@ def __init__(self, read_q: queue.Queue, *args, **kwargs): the queue to where the data received will be put """ self._read_q = read_q + self._buffer = bytearray() super().__init__(*args, **kwargs) def connection_made(self, transport: serial.threaded.ReaderThread) -> None: @@ -47,9 +60,21 @@ def data_received(self, data: bytes) -> None: data : bytes the data received from the serial communication """ - for byte in data: - self._read_q.put(byte) - return super().data_received(data) + self._buffer.extend(data) + while True: + if len(self._buffer) < 2: + # not enough data to read the message type and length + break + + message_type = self._buffer[0] + message_length = self._buffer[1] + total_length = 2 + message_length + if len(self._buffer) < total_length: + break + + frame = self._buffer[:total_length] + self._buffer = self._buffer[total_length:] + self._read_q.put(frame) def connection_lost(self, exc: Union[BaseException, None]) -> None: """ @@ -78,12 +103,14 @@ class HarpSerial: msg_q: queue.Queue event_q: queue.Queue - def __init__(self, serial_port: str, **kwargs): + def __init__(self, serial_port: str, use_buffered_protocol: bool = True, **kwargs): """ Parameters ---------- serial_port : str the serial port used to establish the connection with the Harp device. It must be denoted as `/dev/ttyUSBx` in Linux and `COMx` in Windows, where `x` is the number of the serial port + use_buffered_protocol : bool + whether to use the buffered protocol for reading data """ # Connect to the Harp device self._ser = serial.Serial(serial_port, **kwargs) @@ -96,16 +123,28 @@ def __init__(self, serial_port: str, **kwargs): self.event_q = queue.Queue() # Start the thread with the `HarpSerialProtocol` + self.use_buffered_protocol = use_buffered_protocol + protocol_cls = ( + HarpSerialProtocol if use_buffered_protocol else HarpSerialProtocolOld + ) + self._reader = serial.threaded.ReaderThread( self._ser, - partial(HarpSerialProtocol, self._read_q), + partial(protocol_cls, self._read_q), ) self._reader.start() - transport, protocol = self._reader.connect() + self._reader.connect() + + # Choose parsing method based on protocol + parse_target = ( + self.parse_harp_msgs_threaded_buffered + if use_buffered_protocol + else self.parse_harp_msgs_threaded_per_byte + ) # Start the thread that parses and separates the events from the remaining messages self._parse_thread = threading.Thread( - target=self.parse_harp_msgs_threaded, + target=parse_target, daemon=True, ) self._parse_thread.start() @@ -122,7 +161,24 @@ def write(self, data): """ self._reader.write(data) - def parse_harp_msgs_threaded(self): + def parse_harp_msgs_threaded_buffered(self): + """ + Parses the Harp messages and separates the events from the remaining messages. + """ + while True: + frame = self._read_q.get() + try: + # Parses the bytearray into a ReplyHarpMessage object + msg = HarpMessage.parse(frame) + if msg.message_type == MessageType.EVENT: + self.event_q.put(msg) + else: + self.msg_q.put(msg) + except Exception as e: + self.log.error(f"Error parsing message: {e}") + self.log.debug(f"Raw data: {frame}") + + def parse_harp_msgs_threaded_per_byte(self): """ Parses the Harp messages and separates the events from the remaining messages. """ diff --git a/pyharp/messages.py b/pyharp/protocol/messages.py similarity index 99% rename from pyharp/messages.py rename to pyharp/protocol/messages.py index c7df70b..f03862b 100644 --- a/pyharp/messages.py +++ b/pyharp/protocol/messages.py @@ -3,7 +3,7 @@ import struct from typing import List, Union -from pyharp import MessageType, PayloadType +from pyharp.protocol import MessageType, PayloadType class HarpMessage: diff --git a/tests/test_messages.py b/tests/test_messages.py index cf5ba1d..5b406ac 100644 --- a/tests/test_messages.py +++ b/tests/test_messages.py @@ -1,7 +1,7 @@ import pytest -from pyharp import CommonRegisters, MessageType, PayloadType -from pyharp.messages import ( +from pyharp.protocol import CommonRegisters, MessageType, PayloadType +from pyharp.protocol.messages import ( HarpMessage, ReadHarpMessage, ReplyHarpMessage, From 9e9a95a815a593d1de8fb85aa3263e6834d5e44e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Tue, 20 May 2025 14:24:54 +0100 Subject: [PATCH 114/267] Update project MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- pyproject.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 666bc37..cdb74c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,12 @@ dev = [ requires = ["hatchling"] build-backend = "hatchling.build" +[tool.hatch.build.targets.wheel] +include = [ + "pyharp", + "pyharp/**/*", +] + [tool.ruff.lint.pydocstyle] convention = "numpy" From b8e4bed02146b98ae125c26dac6d956a716396fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Thu, 22 May 2025 10:18:00 +0100 Subject: [PATCH 115/267] Delete behavior driver --- pyharp/drivers/__init__.py | 0 pyharp/drivers/behavior.py | 394 ------------------------------------- 2 files changed, 394 deletions(-) delete mode 100644 pyharp/drivers/__init__.py delete mode 100644 pyharp/drivers/behavior.py diff --git a/pyharp/drivers/__init__.py b/pyharp/drivers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pyharp/drivers/behavior.py b/pyharp/drivers/behavior.py deleted file mode 100644 index 804b7c5..0000000 --- a/pyharp/drivers/behavior.py +++ /dev/null @@ -1,394 +0,0 @@ -"""Behavior Device Driver.""" - -from enum import Enum - -from serial.serialutil import SerialException - -from pyharp.protocol import PayloadType -from pyharp.protocol.device import Device -from pyharp.protocol.messages import ReadHarpMessage, ReplyHarpMessage, WriteHarpMessage - -# These definitions are from app_regs.h in the firmware. -# Type, Base Address, "Description." -REGISTERS = { # RJ45 "PORT" (0, 1, 2) Digital Inputs - "PORT_DIS": ( - PayloadType.U8, - 32, - "Reflects the state of DI digital lines of each Port.", - ), - # Manipulate any of the board's digital outputs. - "OUTPUTS_SET": (PayloadType.U16, 34, "Set the corresponding output."), - "OUTPUTS_CLR": (PayloadType.U16, 35, "Clear the corresponding output."), - "OUTPUTS_TOGGLE": (PayloadType.U16, 36, "Toggle the corresponding output."), - "OUTPUTS_OUT": (PayloadType.U16, 37, "Control corresponding output."), - # RJ45 "PORT" (0, 1, 2) Digital IOs - "PORT_DIOS_SET": (PayloadType.U8, 38, "Set the corresponding DIO."), - "PORT_DIOS_CLEAR": (PayloadType.U8, 39, "Clear the corresponding DIO."), - "PORT_DIOS_TOGGLE": (PayloadType.U8, 40, "Toggle the corresponding DIO."), - "PORT_DIOS_OUT": (PayloadType.U8, 41, "Control the corresponding DIO."), - "PORT_DIOS_CONF": (PayloadType.U8, 42, "Set the DIOs direction (1 is output)."), - "PORT_DIOS_IN": (PayloadType.U8, 43, "State of the DIOs."), - "ADD_REG_DATA": ( - PayloadType.S16, - 44, - "Voltage at ADC input and decoder (poke2) value.", - ), - "EVNT_ENABLE": (PayloadType.U8, 77, "Enable events within the bitfields."), -} - - -# Register Bitfields -class PORT_DIS(Enum): - DI0 = 0 - DI1 = 1 - DI2 = 2 - - -class OUTPUTS_OUT(Enum): - PORT0_DO = 0 - PORT0_D1 = 1 - PORT0_D2 = 2 - - PORT0_12V = 3 - PORT1_12V = 4 - PORT2_12V = 5 - - B_LED0 = 6 - B_LED1 = 7 - B_RGB0 = 8 - B_RGB1 = 9 - - DO0 = 10 - DO1 = 11 - DO2 = 12 - DO3 = 13 - - -class PORT_DIOS_IN(Enum): - DIO0 = 0 - DIO1 = 0 - DIO2 = 0 - - -# reader-friendly events for enabling/disabling. -class Events(Enum): - port_digital_inputs = 0 # PORT_DIS - port_digital_ios = 1 # PORT_DIOS_IN - analog_input = 2 # DATA - cam0 = 3 # CAM0 - cam1 = 3 # CAM1 - - -class Behavior: - """Driver for Behavior Device.""" - - # On Linux, the symlink to the first detected harp device. - # Name set in udev rules and will increment with subsequent devices. - DEVICE_NAME = "Behavior" - DEFAULT_PORT_NAME = "/dev/harp_device_00" - ID = 1216 - - def __init__(self, port_name=None, output_filename=None): - """Class constructor. Connect to a device.""" - - self.device = None - - try: - if port_name is None: - self.device = Device(self.__class__.DEFAULT_PORT_NAME, output_filename) - else: - self.device = Device(port_name, output_filename) - except (FileNotFoundError, SerialException): - print("Error: Failed to connect to Behavior Device. Is it plugged in?") - raise - - if self.device.WHO_AM_I != self.__class__.ID: - raise IOError("Error: Did not connect to Harp Behavior Device.") - - - def get_reg_info(self, reg_name: str) -> str: - """get info for this device's particular reg.""" - try: - return REGISTERS[reg_name][2] - except KeyError: - raise KeyError(f"reg: {reg_name} is not a register in " - "{self.__class__.name} Device's register map.") - - - def disable_all_events(self) -> ReplyHarpMessage: - """Disable the publishing of all events from Behavior device.""" - event_reg_bitmask = (((1 << Events.port_digital_inputs.value) | \ - (1 << Events.port_digital_ios.value) | \ - (1 << Events.analog_input.value) | \ - (1 << Events.cam0.value) | \ - (1 << Events.cam1.value) ) ^ 0xFF) - reg_type, reg_index, _ = REGISTERS["EVNT_ENABLE"] - return self.device.send( - WriteHarpMessage(reg_type, reg_index, event_reg_bitmask) - ) - - - def enable_events(self, *events: Events) -> ReplyHarpMessage: - """enable any events passed in as arguments.""" - event_reg_bitmask = 0x00 - for event in events: - event_reg_bitmask |= (1 << event.value) - reg_type, reg_index, _ = REGISTERS["EVNT_ENABLE"] - return self.device.send( - WriteHarpMessage(reg_type, reg_index, event_reg_bitmask) - ) - - -# Board inputs, outputs, and some settings configured as @properties. - # INPUTS - @property - def all_input_states(self): - """return the state of all PORT digital inputs.""" - reg_type, reg_index, _ = REGISTERS["PORT_DIS"] - return self.device.send(ReadHarpMessage(reg_type, reg_index)).payload - - @property - def DI0(self): - """return the state of port0 digital input 0.""" - return self.all_port_input_states & 0x01 - - @property - def DI1(self): - """return the state of port1 digital input 0.""" - offset = PORT_DIS.DI1.value - return (self.all_port_input_states >> offset) & 0x01 - - @property - def DI2(self): - """return the state of port2 digital input 0.""" - offset = PORT_DIS.DI2.value - return (self.all_input_states >> offset) & 0x01 - -# These do not work currently. Perhaps something needs to be cleared (MIMIC?) -# before they will configure properly. -# # IOs -# def set_io_configuration(self, bitmask : int): -# """set the state of all PORT digital ios. (1 is output.)""" -# reg_type, reg_index, _ = REGISTERS["PORT_DIOS_CONF"] - - # self.device.send(WriteHarpMessage(reg_type, reg_index, bitmask)) -# -# @property -# def all_io_states(self): -# """return the state of all PORT digital ios.""" -# reg_type, reg_index, _ = REGISTERS["PORT_DIOS_IN"] -# read_message_type = self.__class__.READ_MSG_LOOKUP[reg_type] - # return self.device.send(read_message_type(reg_index)).payload -# -# @all_io_states.setter -# def all_io_states(self, bitmask : int): -# """set the state of all PORT digital input/outputs.""" -# # Setting the state of the "DIO" pins, requires writing to the -# # _IN register, which is different from the OUTPUT -# reg_type, reg_index, _ = REGISTERS["PORT_DIOS_IN"] - - # return self.device.send(WriteHarpMessage(reg_type, reg_index, bitmask)) -# -# def set_io_outputs(self, bitmask : int): -# """set digital input/outputs to logic 1 according to bitmask.""" -# reg_type, reg_index, _ = REGISTERS["PORT_DIOS_SET"] - - # return self.device.send(WriteHarpMessage(reg_type, reg_index, bitmask)) -# -# def clear_io_outputs(self, bitmask : int): -# """clear digital input/outputs (specified with logic 1) according to bitmask.""" -# reg_type, reg_index, _ = REGISTERS["PORT_DIOS_CLEAR"] - - # return self.device.send(WriteHarpMessage(reg_type, reg_index, bitmask)) -# -# @property -# def port0_io0(self): -# """read the digital io state.""" -# return self.all_port_io_states & 0x01 -# -# @port0_io0.setter -# def port0_io0(self, value: int): -# """write port0 digital io state.""" -# pass -# -# @property -# def port1_io0(self): -# """read the digital io state.""" -# return (self.all_port_io_states >> 1) & 0x01 -# -# @port0_io0.setter -# def port1_io0(self, value: int): -# """write port0 digital io state.""" -# self.set_outputs(value&0x01) -# -# @property -# def port2_io0(self): -# """read the digital io state.""" -# return (self.all_port_io_states >> 2) & 0x01 -# -# @port0_io0.setter -# def port2_io0(self, value: int): -# """write port0 digital io state.""" -# pass - - - # OUTPUTS - @property - def all_output_states(self): - """return the state of all PORT digital inputs.""" - reg_type, reg_index, _ = REGISTERS["OUTPUTS_OUT"] - return self.device.send(ReadHarpMessage(reg_type, reg_index)).payload - - @all_output_states.setter - def all_output_states(self, bitmask : int): - """set the state of all PORT digital inputs.""" - reg_type, reg_index, _ = REGISTERS["OUTPUTS_OUT"] - return self.device.send(WriteHarpMessage(reg_type, reg_index, bitmask)) - - def set_outputs(self, bitmask : int): - """set digital outputs to logic 1 according to bitmask.""" - reg_type, reg_index, _ = REGISTERS["OUTPUTS_SET"] - return self.device.send(WriteHarpMessage(reg_type, reg_index, bitmask)) - - def clear_outputs(self, bitmask : int): - """clear digital outputs (specified with logic 1) according to bitmask.""" - reg_type, reg_index, _ = REGISTERS["OUTPUTS_CLR"] - return self.device.send(WriteHarpMessage(reg_type, reg_index, bitmask)) - - @property - def D0(self): - """read the digital output D0 state.""" - return (self.all_output_states >> 10) & 0x01 - - @D0.setter - def D0(self, value): - """set the digital output D0 state.""" - if value: - self.set_outputs(1 << 10) - else: - self.clear_outputs(1 << 10) - - @property - def D1(self): - """read the digital output D1 state.""" - return (self.all_output_states >> 11) & 0x01 - - @D1.setter - def D1(self, value): - """set the digital output D1 state.""" - if value: - self.set_outputs(1 << 11) - else: - self.clear_outputs(1 << 11) - - @property - def D2(self): - """read the digital output D2 state.""" - return (self.all_output_states >> 12) & 0x01 - - @D2.setter - def D2(self, value): - """set the digital output D2 state.""" - if value: - self.set_outputs(1 << 12) - else: - self.clear_outputs(1 << 12) - - @property - def D3(self): - """read the digital output D3 state.""" - return (self.all_output_states >> 10) & 0x01 - - @D3.setter - def D3(self, value): - """set the digital output D3 state.""" - if value: - self.set_outputs(1 << 13) - else: - self.clear_outputs(1 << 13) - - @property - def port0_D0(self): - return self.all_output_states & 0x01 - - @port0_D0.setter - def port0_D0(self, value): - if value: - self.set_outputs(1) - else: - self.clear_outputs(1) - - @property - def port1_D0(self): - return (self.all_output_states >> 1) & 0x01 - - @port1_D0.setter - def port1_D0(self, value): - if value: - self.set_outputs(1 << 1) - else: - self.clear_outputs(1 << 1) - - @property - def port2_D0(self): - return (self.all_output_states >> 2) & 0x01 - - @port2_D0.setter - def port2_D0(self, value): - if value: - self.set_outputs(1 << 2) - else: - self.clear_outputs(1 << 2) - - - @property - def port0_12V(self): - return (self.all_output_states >> 3) & 0x01 - - @port0_12V.setter - def port0_12V(self, value): - if value: - self.set_outputs(1 << 3) - else: - self.clear_outputs(1 << 3) - - @property - def port1_12V(self): - return (self.all_output_states >> 4) & 0x01 - - @port1_12V.setter - def port1_12V(self, value): - if value: - self.set_outputs(1 << 4) - else: - self.clear_outputs(1 << 4) - - @property - def port2_12V(self): - return (self.all_output_states >> 5) & 0x01 - - @port2_12V.setter - def port2_12V(self, value): - if value: - self.set_outputs(1 << 5) - else: - self.clear_outputs(1 << 5) - - - - - def __enter__(self): - """Setup for the 'with' statement""" - return self - - - def __exit__(self, *args): - """Cleanup for the 'with' statement""" - if self.device is not None: - self.device.disconnect() - - - def __del__(self): - """Cleanup when Device gets garbage collected.""" - if self.device is not None: - self.device.disconnect() From 9aedecfeca403c377595de1a5dbb08b89a86490a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Thu, 22 May 2025 13:05:03 +0100 Subject: [PATCH 116/267] Move device.py and harp_serial.py to communication folder --- .../olfactometer_example.py | 2 +- .../read_and_write_from_registers.py | 2 +- .../wait_for_events/wait_for_events.py | 2 +- pyharp/communication/__init__.py | 2 + pyharp/{protocol => communication}/device.py | 3 +- .../harp_serial.py | 61 ++----------------- 6 files changed, 10 insertions(+), 62 deletions(-) create mode 100644 pyharp/communication/__init__.py rename pyharp/{protocol => communication}/device.py (99%) rename pyharp/{protocol => communication}/harp_serial.py (65%) diff --git a/docs/examples/olfactometer_example/olfactometer_example.py b/docs/examples/olfactometer_example/olfactometer_example.py index 243e3c2..1b7017b 100644 --- a/docs/examples/olfactometer_example/olfactometer_example.py +++ b/docs/examples/olfactometer_example/olfactometer_example.py @@ -4,8 +4,8 @@ from serial import SerialException +from pyharp.communication.device import Device, OperationMode from pyharp.protocol import MessageType, PayloadType -from pyharp.protocol.device import Device, OperationMode from pyharp.protocol.messages import HarpMessage SERIAL_PORT = ( diff --git a/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py b/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py index 0858bb6..2f21927 100755 --- a/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py +++ b/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py @@ -1,7 +1,7 @@ from serial import SerialException +from pyharp.devices.device import Device from pyharp.protocol import MessageType, PayloadType -from pyharp.protocol.device import Device from pyharp.protocol.messages import HarpMessage SERIAL_PORT = ( diff --git a/docs/examples/wait_for_events/wait_for_events.py b/docs/examples/wait_for_events/wait_for_events.py index a33c59a..432fed9 100755 --- a/docs/examples/wait_for_events/wait_for_events.py +++ b/docs/examples/wait_for_events/wait_for_events.py @@ -1,5 +1,5 @@ +from pyharp.communication.device import Device from pyharp.protocol import OperationMode -from pyharp.protocol.device import Device SERIAL_PORT = ( "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port) diff --git a/pyharp/communication/__init__.py b/pyharp/communication/__init__.py new file mode 100644 index 0000000..7af9dc5 --- /dev/null +++ b/pyharp/communication/__init__.py @@ -0,0 +1,2 @@ +from .device import Device as Device +from .harp_serial import HarpSerial as HarpSerial diff --git a/pyharp/protocol/device.py b/pyharp/communication/device.py similarity index 99% rename from pyharp/protocol/device.py rename to pyharp/communication/device.py index 7125c73..7718f24 100644 --- a/pyharp/protocol/device.py +++ b/pyharp/communication/device.py @@ -8,6 +8,7 @@ import serial +from pyharp.communication import HarpSerial from pyharp.protocol import ( ClockConfig, CommonRegisters, @@ -18,7 +19,6 @@ ResetMode, ) from pyharp.protocol.device_names import device_names -from pyharp.protocol.harp_serial import HarpSerial from pyharp.protocol.messages import HarpMessage, ReplyHarpMessage @@ -140,7 +140,6 @@ def connect(self) -> None: """ self._ser = HarpSerial( self._serial_port, # "/dev/tty.usbserial-A106C8O9" - use_buffered_protocol=False, baudrate=1000000, timeout=self._TIMEOUT_S, parity=serial.PARITY_NONE, diff --git a/pyharp/protocol/harp_serial.py b/pyharp/communication/harp_serial.py similarity index 65% rename from pyharp/protocol/harp_serial.py rename to pyharp/communication/harp_serial.py index f264f34..738ec06 100644 --- a/pyharp/protocol/harp_serial.py +++ b/pyharp/communication/harp_serial.py @@ -10,18 +10,6 @@ from pyharp.protocol.messages import HarpMessage, MessageType -class HarpSerialProtocolOld(serial.threaded.Protocol): - # Old implementation (per-byte queue) - def __init__(self, read_q: queue.Queue, *args, **kwargs): - self._read_q = read_q - super().__init__(*args, **kwargs) - - def data_received(self, data: bytes) -> None: - for byte in data: - self._read_q.put(byte) - return super().data_received(data) - - class HarpSerialProtocol(serial.threaded.Protocol): """ The `HarpSerialProtocol` class deals with the data received from the serial communication. @@ -66,7 +54,7 @@ def data_received(self, data: bytes) -> None: # not enough data to read the message type and length break - message_type = self._buffer[0] + # Read length (we can ignore the message type) message_length = self._buffer[1] total_length = 2 + message_length if len(self._buffer) < total_length: @@ -103,14 +91,12 @@ class HarpSerial: msg_q: queue.Queue event_q: queue.Queue - def __init__(self, serial_port: str, use_buffered_protocol: bool = True, **kwargs): + def __init__(self, serial_port: str, **kwargs): """ Parameters ---------- serial_port : str the serial port used to establish the connection with the Harp device. It must be denoted as `/dev/ttyUSBx` in Linux and `COMx` in Windows, where `x` is the number of the serial port - use_buffered_protocol : bool - whether to use the buffered protocol for reading data """ # Connect to the Harp device self._ser = serial.Serial(serial_port, **kwargs) @@ -123,28 +109,16 @@ def __init__(self, serial_port: str, use_buffered_protocol: bool = True, **kwarg self.event_q = queue.Queue() # Start the thread with the `HarpSerialProtocol` - self.use_buffered_protocol = use_buffered_protocol - protocol_cls = ( - HarpSerialProtocol if use_buffered_protocol else HarpSerialProtocolOld - ) - self._reader = serial.threaded.ReaderThread( self._ser, - partial(protocol_cls, self._read_q), + partial(HarpSerialProtocol, self._read_q), ) self._reader.start() self._reader.connect() - # Choose parsing method based on protocol - parse_target = ( - self.parse_harp_msgs_threaded_buffered - if use_buffered_protocol - else self.parse_harp_msgs_threaded_per_byte - ) - # Start the thread that parses and separates the events from the remaining messages self._parse_thread = threading.Thread( - target=parse_target, + target=self.parse_harp_msgs_threaded_buffered, daemon=True, ) self._parse_thread.start() @@ -177,30 +151,3 @@ def parse_harp_msgs_threaded_buffered(self): except Exception as e: self.log.error(f"Error parsing message: {e}") self.log.debug(f"Raw data: {frame}") - - def parse_harp_msgs_threaded_per_byte(self): - """ - Parses the Harp messages and separates the events from the remaining messages. - """ - while True: - # Gets the Harp message bytes based on the length byte of the message - message_type = self._read_q.get(1) - message_length = self._read_q.get(1) - message_content = bytes([self._read_q.get() for _ in range(message_length)]) - self.log.debug(f"reply (type): {message_type}") - self.log.debug(f"reply (length): {message_length}") - self.log.debug(f"reply (payload): {message_content}") - - # Reconstructs the message into a bytearray - frame = bytearray() - frame.append(message_type) - frame.append(message_length) - frame += message_content - # Parses the bytearray into a ReplyHarpMessage object - msg = HarpMessage.parse(frame) - - # Puts the parsed Harp message into the correct queue - if msg.message_type == MessageType.EVENT: - self.event_q.put(msg) - else: - self.msg_q.put(msg) From 2ff6bc2bc5d45d0b0ebc02b466b1ca675a902547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Thu, 22 May 2025 13:06:11 +0100 Subject: [PATCH 117/267] Move core from docs to protocol --- docs/api/core.md | 4 ---- docs/api/protocol.md | 7 +++++++ mkdocs.yml | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) delete mode 100644 docs/api/core.md create mode 100644 docs/api/protocol.md diff --git a/docs/api/core.md b/docs/api/core.md deleted file mode 100644 index 6ec54e7..0000000 --- a/docs/api/core.md +++ /dev/null @@ -1,4 +0,0 @@ -::: pyharp.MessageType -::: pyharp.PayloadType -::: pyharp.CommonRegisters -::: pyharp.OperationMode diff --git a/docs/api/protocol.md b/docs/api/protocol.md new file mode 100644 index 0000000..9841eec --- /dev/null +++ b/docs/api/protocol.md @@ -0,0 +1,7 @@ +::: pyharp.protocol.MessageType +::: pyharp.protocol.PayloadType +::: pyharp.protocol.CommonRegisters +::: pyharp.protocol.OperationMode +::: pyharp.protocol.OperationCtrl +::: pyharp.protocol.ResetMode +::: pyharp.protocol.ClockConfig diff --git a/mkdocs.yml b/mkdocs.yml index 19c56c7..566edef 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -82,7 +82,7 @@ nav: - Read and Write from Registers: examples/read_and_write_from_registers/read_and_write_from_registers.md - Olfactometer Example: examples/olfactometer_example/olfactometer_example.md - API: - - Core: api/core.md + - Protocol: api/protocol.md - Device: api/device.md - Messages: api/messages.md - Devices: '*include ./pyharp.devices/*/mkdocs.yml' From 1ae9aee219124bd6889b8c9980c375c145d9d617 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Thu, 22 May 2025 13:06:34 +0100 Subject: [PATCH 118/267] Update docs links with correct namespaces --- docs/api/device.md | 2 +- docs/api/messages.md | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/api/device.md b/docs/api/device.md index 842f814..42b9b4a 100644 --- a/docs/api/device.md +++ b/docs/api/device.md @@ -1 +1 @@ -::: pyharp.device.Device +::: pyharp.communication.Device diff --git a/docs/api/messages.md b/docs/api/messages.md index 9d85270..569adf5 100644 --- a/docs/api/messages.md +++ b/docs/api/messages.md @@ -1,4 +1,4 @@ -::: pyharp.messages.HarpMessage -::: pyharp.messages.ReplyHarpMessage -::: pyharp.messages.ReadHarpMessage -::: pyharp.messages.WriteHarpMessage +::: pyharp.protocol.messages.HarpMessage +::: pyharp.protocol.messages.ReplyHarpMessage +::: pyharp.protocol.messages.ReadHarpMessage +::: pyharp.protocol.messages.WriteHarpMessage From 55f38d3d5d6bf2700e6d2a8a87df09188144bbf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Thu, 22 May 2025 13:13:49 +0100 Subject: [PATCH 119/267] Remove duplicated extension from mkdocs config --- mkdocs.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 566edef..debd540 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -26,7 +26,6 @@ markdown_extensions: - attr_list - admonition - pymdownx.details - - pymdownx.superfences - pymdownx.emoji: emoji_index: !!python/name:material.extensions.emoji.twemoji emoji_generator: !!python/name:material.extensions.emoji.to_svg From c1d1a5941dd40fae05de27a3d5d0be459cf8f9ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Thu, 22 May 2025 13:29:48 +0100 Subject: [PATCH 120/267] Minor fix on example --- .../read_and_write_from_registers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py b/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py index 2f21927..4ebbb94 100755 --- a/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py +++ b/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py @@ -1,6 +1,6 @@ from serial import SerialException -from pyharp.devices.device import Device +from pyharp.communication.device import Device from pyharp.protocol import MessageType, PayloadType from pyharp.protocol.messages import HarpMessage From deef783d2b4cddb571e5614fee441592f51d9372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Fri, 23 May 2025 09:09:51 +0100 Subject: [PATCH 121/267] Update files to prevent circular imports --- pyharp/communication/device.py | 2 +- pyharp/protocol/__init__.py | 10 +--------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/pyharp/communication/device.py b/pyharp/communication/device.py index 7718f24..b66e5fc 100644 --- a/pyharp/communication/device.py +++ b/pyharp/communication/device.py @@ -8,7 +8,7 @@ import serial -from pyharp.communication import HarpSerial +from pyharp.communication.harp_serial import HarpSerial from pyharp.protocol import ( ClockConfig, CommonRegisters, diff --git a/pyharp/protocol/__init__.py b/pyharp/protocol/__init__.py index 2f73504..12e69f4 100644 --- a/pyharp/protocol/__init__.py +++ b/pyharp/protocol/__init__.py @@ -1,9 +1 @@ -from .base import ( - ClockConfig, - CommonRegisters, - MessageType, - OperationCtrl, - OperationMode, - PayloadType, - ResetMode, -) +from .base import * # noqa: F403 From 74daef6447e439380ef33cfd8b97f61468e01012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Thu, 12 Jun 2025 08:13:57 +0100 Subject: [PATCH 122/267] Update project related information for pushing it to PyPI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- LICENSE | 2 +- README.md | 1 + pyharp/communication/device.py | 2 +- pyproject.toml | 20 ++++++++++++++++---- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/LICENSE b/LICENSE index 86b9923..0c33b6f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 OEPS & Filipe Carvalho +Copyright (c) 2025 Hardware and Software Platform, Champalimaud Foundation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 79b36f3..cd80ce8 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ from pyharp.messages import HarpMessage # Connect to a device device = Device("/dev/ttyUSB0") +#device = Device("COM3") # for Windows # Get device information device.info() diff --git a/pyharp/communication/device.py b/pyharp/communication/device.py index b66e5fc..c6b2df2 100644 --- a/pyharp/communication/device.py +++ b/pyharp/communication/device.py @@ -434,7 +434,7 @@ def send(self, message: HarpMessage) -> Optional[ReplyHarpMessage]: Parameters ---------- - message_bytes : HarpMessage + message : HarpMessage the HarpMessage containing the message to be sent to the device Returns diff --git a/pyproject.toml b/pyproject.toml index cdb74c8..e7c93f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,15 +1,21 @@ [project] -name = "pyharp" -version = "0.1.0" +name = "harp-protocol" +version = "0.2.0a1" description = "Library for data acquisition and control of devices implementing the Harp protocol." -authors = [{ name= "Filipe Carvalho", email="filipe@open-ephys.org"}] +authors = [{ name= "Hardware and Software Platform, Champalimaud Foundation", email="software@research.fchampalimaud.org"}] license = "MIT" readme = 'README.md' -requires-python = ">=3.11" +keywords = ['python', 'harp'] +requires-python = ">=3.9,<4.0" dependencies = [ "pyserial>=3.5", ] +[project.urls] +Repository = "https://github.com/fchampalimaud/pyharp/" +"Bug Tracker" = "https://github.com/fchampalimaud/pyharp/issues" +Documentation = "https://fchampalimaud.github.io/pyharp/" + [dependency-groups] dev = [ "mkdocs>=1.6.1", @@ -44,3 +50,9 @@ python_files = [ "tests.py", "test_*.py" ] + +[[tool.uv.index]] +name = "testpypi" +url = "https://test.pypi.org/simple/" +publish-url = "https://test.pypi.org/legacy/" +explicit = true From 60bff1237267bba4e1e90cfcea67dcef665a8d68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Mon, 30 Jun 2025 10:42:09 +0100 Subject: [PATCH 123/267] Change namespace to "harp" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- README.md | 18 +++++++++--------- docs/api/device.md | 2 +- docs/api/messages.md | 8 ++++---- docs/api/protocol.md | 14 +++++++------- docs/examples/index.md | 2 +- .../olfactometer_example.py | 6 +++--- .../read_and_write_from_registers.py | 6 +++--- .../wait_for_events/wait_for_events.py | 4 ++-- docs/index.md | 18 +++++++++--------- {pyharp => harp}/communication/__init__.py | 0 {pyharp => harp}/communication/device.py | 8 ++++---- {pyharp => harp}/communication/harp_serial.py | 2 +- {pyharp => harp}/devices/.gitignore | 0 {pyharp => harp}/protocol/__init__.py | 0 {pyharp => harp}/protocol/base.py | 0 {pyharp => harp}/protocol/device_names.py | 2 +- harp/protocol/exceptions.py | 18 ++++++++++++++++++ {pyharp => harp}/protocol/messages.py | 2 +- mkdocs.yml | 2 +- pyharp/protocol/exceptions.py | 4 ---- pyproject.toml | 4 ++-- tests/test_messages.py | 4 ++-- 22 files changed, 69 insertions(+), 55 deletions(-) rename {pyharp => harp}/communication/__init__.py (100%) rename {pyharp => harp}/communication/device.py (99%) rename {pyharp => harp}/communication/harp_serial.py (98%) rename {pyharp => harp}/devices/.gitignore (100%) rename {pyharp => harp}/protocol/__init__.py (100%) rename {pyharp => harp}/protocol/base.py (100%) rename {pyharp => harp}/protocol/device_names.py (98%) create mode 100644 harp/protocol/exceptions.py rename {pyharp => harp}/protocol/messages.py (99%) delete mode 100644 pyharp/protocol/exceptions.py diff --git a/README.md b/README.md index cd80ce8..7bf37af 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,21 @@ -# pyharp +# harp Python implementation of the Harp protocol for hardware control and data acquisition. ## Installation ```bash -uv add pyharp +uv add harp-protocol # or -pip install pyharp +pip install harp-protocol ``` ## Quick Start ```python -from pyharp import MessageType, PayloadType -from pyharp.device import Device -from pyharp.messages import HarpMessage +from harp import MessageType, PayloadType +from harp.device import Device +from harp.messages import HarpMessage # Connect to a device device = Device("/dev/ttyUSB0") @@ -40,9 +40,9 @@ device.disconnect() or using the `with` statement: ```python -from pyharp import MessageType, PayloadType -from pyharp.device import Device -from pyharp.messages import HarpMessage +from harp import MessageType, PayloadType +from harp.device import Device +from harp.messages import HarpMessage with Device("/dev/ttyUSB0") as device: # Get device information diff --git a/docs/api/device.md b/docs/api/device.md index 42b9b4a..451d0eb 100644 --- a/docs/api/device.md +++ b/docs/api/device.md @@ -1 +1 @@ -::: pyharp.communication.Device +::: harp.communication.Device diff --git a/docs/api/messages.md b/docs/api/messages.md index 569adf5..0621fd0 100644 --- a/docs/api/messages.md +++ b/docs/api/messages.md @@ -1,4 +1,4 @@ -::: pyharp.protocol.messages.HarpMessage -::: pyharp.protocol.messages.ReplyHarpMessage -::: pyharp.protocol.messages.ReadHarpMessage -::: pyharp.protocol.messages.WriteHarpMessage +::: harp.protocol.messages.HarpMessage +::: harp.protocol.messages.ReplyHarpMessage +::: harp.protocol.messages.ReadHarpMessage +::: harp.protocol.messages.WriteHarpMessage diff --git a/docs/api/protocol.md b/docs/api/protocol.md index 9841eec..4e54672 100644 --- a/docs/api/protocol.md +++ b/docs/api/protocol.md @@ -1,7 +1,7 @@ -::: pyharp.protocol.MessageType -::: pyharp.protocol.PayloadType -::: pyharp.protocol.CommonRegisters -::: pyharp.protocol.OperationMode -::: pyharp.protocol.OperationCtrl -::: pyharp.protocol.ResetMode -::: pyharp.protocol.ClockConfig +::: harp.protocol.MessageType +::: harp.protocol.PayloadType +::: harp.protocol.CommonRegisters +::: harp.protocol.OperationMode +::: harp.protocol.OperationCtrl +::: harp.protocol.ResetMode +::: harp.protocol.ClockConfig diff --git a/docs/examples/index.md b/docs/examples/index.md index f66468a..d045e5d 100644 --- a/docs/examples/index.md +++ b/docs/examples/index.md @@ -1,6 +1,6 @@ # Examples -This section contains some examples to help you get started with `pyharp`. +This section contains some examples to help you get started with `harp`. Here's the complete list of available examples: diff --git a/docs/examples/olfactometer_example/olfactometer_example.py b/docs/examples/olfactometer_example/olfactometer_example.py index 1b7017b..6078a77 100644 --- a/docs/examples/olfactometer_example/olfactometer_example.py +++ b/docs/examples/olfactometer_example/olfactometer_example.py @@ -4,9 +4,9 @@ from serial import SerialException -from pyharp.communication.device import Device, OperationMode -from pyharp.protocol import MessageType, PayloadType -from pyharp.protocol.messages import HarpMessage +from harp.communication.device import Device, OperationMode +from harp.protocol import MessageType, PayloadType +from harp.protocol.messages import HarpMessage SERIAL_PORT = ( "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port) diff --git a/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py b/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py index 4ebbb94..a43019f 100755 --- a/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py +++ b/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py @@ -1,8 +1,8 @@ from serial import SerialException -from pyharp.communication.device import Device -from pyharp.protocol import MessageType, PayloadType -from pyharp.protocol.messages import HarpMessage +from harp.communication.device import Device +from harp.protocol import MessageType, PayloadType +from harp.protocol.messages import HarpMessage SERIAL_PORT = ( "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port) diff --git a/docs/examples/wait_for_events/wait_for_events.py b/docs/examples/wait_for_events/wait_for_events.py index 432fed9..7946464 100755 --- a/docs/examples/wait_for_events/wait_for_events.py +++ b/docs/examples/wait_for_events/wait_for_events.py @@ -1,5 +1,5 @@ -from pyharp.communication.device import Device -from pyharp.protocol import OperationMode +from harp.communication.device import Device +from harp.protocol import OperationMode SERIAL_PORT = ( "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port) diff --git a/docs/index.md b/docs/index.md index 79b36f3..5dd1923 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,21 +1,21 @@ -# pyharp +# harp Python implementation of the Harp protocol for hardware control and data acquisition. ## Installation ```bash -uv add pyharp +uv add harp-protocol # or -pip install pyharp +pip install harp-protocol ``` ## Quick Start ```python -from pyharp import MessageType, PayloadType -from pyharp.device import Device -from pyharp.messages import HarpMessage +from harp import MessageType, PayloadType +from harp.device import Device +from harp.messages import HarpMessage # Connect to a device device = Device("/dev/ttyUSB0") @@ -39,9 +39,9 @@ device.disconnect() or using the `with` statement: ```python -from pyharp import MessageType, PayloadType -from pyharp.device import Device -from pyharp.messages import HarpMessage +from harp import MessageType, PayloadType +from harp.device import Device +from harp.messages import HarpMessage with Device("/dev/ttyUSB0") as device: # Get device information diff --git a/pyharp/communication/__init__.py b/harp/communication/__init__.py similarity index 100% rename from pyharp/communication/__init__.py rename to harp/communication/__init__.py diff --git a/pyharp/communication/device.py b/harp/communication/device.py similarity index 99% rename from pyharp/communication/device.py rename to harp/communication/device.py index c6b2df2..5647293 100644 --- a/pyharp/communication/device.py +++ b/harp/communication/device.py @@ -8,8 +8,8 @@ import serial -from pyharp.communication.harp_serial import HarpSerial -from pyharp.protocol import ( +from harp.communication.harp_serial import HarpSerial +from harp.protocol import ( ClockConfig, CommonRegisters, MessageType, @@ -18,8 +18,8 @@ PayloadType, ResetMode, ) -from pyharp.protocol.device_names import device_names -from pyharp.protocol.messages import HarpMessage, ReplyHarpMessage +from harp.protocol.device_names import device_names +from harp.protocol.messages import HarpMessage, ReplyHarpMessage class Device: diff --git a/pyharp/communication/harp_serial.py b/harp/communication/harp_serial.py similarity index 98% rename from pyharp/communication/harp_serial.py rename to harp/communication/harp_serial.py index 738ec06..d5d5578 100644 --- a/pyharp/communication/harp_serial.py +++ b/harp/communication/harp_serial.py @@ -7,7 +7,7 @@ import serial import serial.threaded -from pyharp.protocol.messages import HarpMessage, MessageType +from harp.protocol.messages import HarpMessage, MessageType class HarpSerialProtocol(serial.threaded.Protocol): diff --git a/pyharp/devices/.gitignore b/harp/devices/.gitignore similarity index 100% rename from pyharp/devices/.gitignore rename to harp/devices/.gitignore diff --git a/pyharp/protocol/__init__.py b/harp/protocol/__init__.py similarity index 100% rename from pyharp/protocol/__init__.py rename to harp/protocol/__init__.py diff --git a/pyharp/protocol/base.py b/harp/protocol/base.py similarity index 100% rename from pyharp/protocol/base.py rename to harp/protocol/base.py diff --git a/pyharp/protocol/device_names.py b/harp/protocol/device_names.py similarity index 98% rename from pyharp/protocol/device_names.py rename to harp/protocol/device_names.py index c166ff0..4b7efcb 100644 --- a/pyharp/protocol/device_names.py +++ b/harp/protocol/device_names.py @@ -1,6 +1,6 @@ from collections import defaultdict -# This file contains the device names for the current version of the pyHarp library. +# This file contains the device names for the current version of the harp library. # These names were extracted from https://github.com/harp-tech/protocol/blob/main/whoami.yml # commit used: https://github.com/harp-tech/protocol/commit/3e2a228 diff --git a/harp/protocol/exceptions.py b/harp/protocol/exceptions.py new file mode 100644 index 0000000..058e23b --- /dev/null +++ b/harp/protocol/exceptions.py @@ -0,0 +1,18 @@ +class HarpException(Exception): + """Base class for all exceptions raised related with Harp.""" + + pass + + +class HarpWriteException(HarpException): + def __init__(self, register, message): + super().__init__(f"Error writing to register {register}: {message}") + self.register = register + self.message = message + + +class HarpReadException(HarpException): + def __init__(self, register, message): + super().__init__(f"Error reading from register {register}: {message}") + self.register = register + self.message = message diff --git a/pyharp/protocol/messages.py b/harp/protocol/messages.py similarity index 99% rename from pyharp/protocol/messages.py rename to harp/protocol/messages.py index f03862b..4437e82 100644 --- a/pyharp/protocol/messages.py +++ b/harp/protocol/messages.py @@ -3,7 +3,7 @@ import struct from typing import List, Union -from pyharp.protocol import MessageType, PayloadType +from harp.protocol import MessageType, PayloadType class HarpMessage: diff --git a/mkdocs.yml b/mkdocs.yml index debd540..de3b1b3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -84,7 +84,7 @@ nav: - Protocol: api/protocol.md - Device: api/device.md - Messages: api/messages.md - - Devices: '*include ./pyharp.devices/*/mkdocs.yml' + - Devices: '*include ./harp.devices/*/mkdocs.yml' extra_css: - stylesheets/extra.css diff --git a/pyharp/protocol/exceptions.py b/pyharp/protocol/exceptions.py deleted file mode 100644 index 81cd8ae..0000000 --- a/pyharp/protocol/exceptions.py +++ /dev/null @@ -1,4 +0,0 @@ -class HarpException(Exception): - """Base class for all exceptions raised by pyHARP.""" - - pass diff --git a/pyproject.toml b/pyproject.toml index e7c93f8..934a2a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,8 +36,8 @@ build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] include = [ - "pyharp", - "pyharp/**/*", + "harp", + "harp/**/*", ] [tool.ruff.lint.pydocstyle] diff --git a/tests/test_messages.py b/tests/test_messages.py index 5b406ac..45ab698 100644 --- a/tests/test_messages.py +++ b/tests/test_messages.py @@ -1,7 +1,7 @@ import pytest -from pyharp.protocol import CommonRegisters, MessageType, PayloadType -from pyharp.protocol.messages import ( +from harp.protocol import CommonRegisters, MessageType, PayloadType +from harp.protocol.messages import ( HarpMessage, ReadHarpMessage, ReplyHarpMessage, From f50be3246945987c43d4df54a6fbcf89404a8545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Mon, 30 Jun 2025 10:44:28 +0100 Subject: [PATCH 124/267] Update minimum Python version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- uv.lock | 226 ++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 186 insertions(+), 40 deletions(-) diff --git a/uv.lock b/uv.lock index de644e6..26c81af 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 2 -requires-python = ">=3.11" +requires-python = ">=3.9, <4.0" [[package]] name = "babel" @@ -39,6 +39,19 @@ version = "3.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188, upload-time = "2024-12-24T18:12:35.43Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013, upload-time = "2024-12-24T18:09:43.671Z" }, + { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285, upload-time = "2024-12-24T18:09:48.113Z" }, + { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449, upload-time = "2024-12-24T18:09:50.845Z" }, + { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892, upload-time = "2024-12-24T18:09:52.078Z" }, + { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123, upload-time = "2024-12-24T18:09:54.575Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943, upload-time = "2024-12-24T18:09:57.324Z" }, + { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063, upload-time = "2024-12-24T18:09:59.794Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578, upload-time = "2024-12-24T18:10:02.357Z" }, + { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629, upload-time = "2024-12-24T18:10:03.678Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778, upload-time = "2024-12-24T18:10:06.197Z" }, + { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453, upload-time = "2024-12-24T18:10:08.848Z" }, + { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479, upload-time = "2024-12-24T18:10:10.044Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790, upload-time = "2024-12-24T18:10:11.323Z" }, { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995, upload-time = "2024-12-24T18:10:12.838Z" }, { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471, upload-time = "2024-12-24T18:10:14.101Z" }, { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831, upload-time = "2024-12-24T18:10:15.512Z" }, @@ -78,6 +91,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732, upload-time = "2024-12-24T18:11:22.774Z" }, { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391, upload-time = "2024-12-24T18:11:24.139Z" }, { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702, upload-time = "2024-12-24T18:11:26.535Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c0/b913f8f02836ed9ab32ea643c6fe4d3325c3d8627cf6e78098671cafff86/charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", size = 197867, upload-time = "2024-12-24T18:12:10.438Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6c/2bee440303d705b6fb1e2ec789543edec83d32d258299b16eed28aad48e0/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", size = 141385, upload-time = "2024-12-24T18:12:11.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/04/cb42585f07f6f9fd3219ffb6f37d5a39b4fd2db2355b23683060029c35f7/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", size = 151367, upload-time = "2024-12-24T18:12:13.177Z" }, + { url = "https://files.pythonhosted.org/packages/54/54/2412a5b093acb17f0222de007cc129ec0e0df198b5ad2ce5699355269dfe/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", size = 143928, upload-time = "2024-12-24T18:12:14.497Z" }, + { url = "https://files.pythonhosted.org/packages/5a/6d/e2773862b043dcf8a221342954f375392bb2ce6487bcd9f2c1b34e1d6781/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", size = 146203, upload-time = "2024-12-24T18:12:15.731Z" }, + { url = "https://files.pythonhosted.org/packages/b9/f8/ca440ef60d8f8916022859885f231abb07ada3c347c03d63f283bec32ef5/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", size = 148082, upload-time = "2024-12-24T18:12:18.641Z" }, + { url = "https://files.pythonhosted.org/packages/04/d2/42fd330901aaa4b805a1097856c2edf5095e260a597f65def493f4b8c833/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", size = 142053, upload-time = "2024-12-24T18:12:20.036Z" }, + { url = "https://files.pythonhosted.org/packages/9e/af/3a97a4fa3c53586f1910dadfc916e9c4f35eeada36de4108f5096cb7215f/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", size = 150625, upload-time = "2024-12-24T18:12:22.804Z" }, + { url = "https://files.pythonhosted.org/packages/26/ae/23d6041322a3556e4da139663d02fb1b3c59a23ab2e2b56432bd2ad63ded/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", size = 153549, upload-time = "2024-12-24T18:12:24.163Z" }, + { url = "https://files.pythonhosted.org/packages/94/22/b8f2081c6a77cb20d97e57e0b385b481887aa08019d2459dc2858ed64871/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", size = 150945, upload-time = "2024-12-24T18:12:25.415Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/c5ec5092747f801b8b093cdf5610e732b809d6cb11f4c51e35fc28d1d389/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", size = 146595, upload-time = "2024-12-24T18:12:28.03Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5a/0b59704c38470df6768aa154cc87b1ac7c9bb687990a1559dc8765e8627e/charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", size = 95453, upload-time = "2024-12-24T18:12:29.569Z" }, + { url = "https://files.pythonhosted.org/packages/85/2d/a9790237cb4d01a6d57afadc8573c8b73c609ade20b80f4cda30802009ee/charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", size = 102811, upload-time = "2024-12-24T18:12:30.83Z" }, { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767, upload-time = "2024-12-24T18:12:32.852Z" }, ] @@ -108,6 +134,16 @@ version = "7.8.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872, upload-time = "2025-03-30T20:36:45.376Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/78/01/1c5e6ee4ebaaa5e079db933a9a45f61172048c7efa06648445821a201084/coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe", size = 211379, upload-time = "2025-03-30T20:34:53.904Z" }, + { url = "https://files.pythonhosted.org/packages/e9/16/a463389f5ff916963471f7c13585e5f38c6814607306b3cb4d6b4cf13384/coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28", size = 211814, upload-time = "2025-03-30T20:34:56.959Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b1/77062b0393f54d79064dfb72d2da402657d7c569cfbc724d56ac0f9c67ed/coverage-7.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3", size = 240937, upload-time = "2025-03-30T20:34:58.751Z" }, + { url = "https://files.pythonhosted.org/packages/d7/54/c7b00a23150083c124e908c352db03bcd33375494a4beb0c6d79b35448b9/coverage-7.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676", size = 238849, upload-time = "2025-03-30T20:35:00.521Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ec/a6b7cfebd34e7b49f844788fda94713035372b5200c23088e3bbafb30970/coverage-7.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d", size = 239986, upload-time = "2025-03-30T20:35:02.307Z" }, + { url = "https://files.pythonhosted.org/packages/21/8c/c965ecef8af54e6d9b11bfbba85d4f6a319399f5f724798498387f3209eb/coverage-7.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a", size = 239896, upload-time = "2025-03-30T20:35:04.141Z" }, + { url = "https://files.pythonhosted.org/packages/40/83/070550273fb4c480efa8381735969cb403fa8fd1626d74865bfaf9e4d903/coverage-7.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c", size = 238613, upload-time = "2025-03-30T20:35:05.889Z" }, + { url = "https://files.pythonhosted.org/packages/07/76/fbb2540495b01d996d38e9f8897b861afed356be01160ab4e25471f4fed1/coverage-7.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f", size = 238909, upload-time = "2025-03-30T20:35:07.76Z" }, + { url = "https://files.pythonhosted.org/packages/a3/7e/76d604db640b7d4a86e5dd730b73e96e12a8185f22b5d0799025121f4dcb/coverage-7.8.0-cp310-cp310-win32.whl", hash = "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f", size = 213948, upload-time = "2025-03-30T20:35:09.144Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a7/f8ce4aafb4a12ab475b56c76a71a40f427740cf496c14e943ade72e25023/coverage-7.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23", size = 214844, upload-time = "2025-03-30T20:35:10.734Z" }, { url = "https://files.pythonhosted.org/packages/2b/77/074d201adb8383addae5784cb8e2dac60bb62bfdf28b2b10f3a3af2fda47/coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27", size = 211493, upload-time = "2025-03-30T20:35:12.286Z" }, { url = "https://files.pythonhosted.org/packages/a9/89/7a8efe585750fe59b48d09f871f0e0c028a7b10722b2172dfe021fa2fdd4/coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea", size = 211921, upload-time = "2025-03-30T20:35:14.18Z" }, { url = "https://files.pythonhosted.org/packages/e9/ef/96a90c31d08a3f40c49dbe897df4f1fd51fb6583821a1a1c5ee30cc8f680/coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7", size = 244556, upload-time = "2025-03-30T20:35:15.616Z" }, @@ -148,6 +184,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/58/e9/8fb8e0ff6bef5e170ee19d59ca694f9001b2ec085dc99b4f65c128bb3f9a/coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883", size = 255116, upload-time = "2025-03-30T20:36:18.033Z" }, { url = "https://files.pythonhosted.org/packages/56/b0/d968ecdbe6fe0a863de7169bbe9e8a476868959f3af24981f6a10d2b6924/coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada", size = 214909, upload-time = "2025-03-30T20:36:19.644Z" }, { url = "https://files.pythonhosted.org/packages/87/e9/d6b7ef9fecf42dfb418d93544af47c940aa83056c49e6021a564aafbc91f/coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257", size = 216068, upload-time = "2025-03-30T20:36:21.282Z" }, + { url = "https://files.pythonhosted.org/packages/60/0c/5da94be095239814bf2730a28cffbc48d6df4304e044f80d39e1ae581997/coverage-7.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa260de59dfb143af06dcf30c2be0b200bed2a73737a8a59248fcb9fa601ef0f", size = 211377, upload-time = "2025-03-30T20:36:23.298Z" }, + { url = "https://files.pythonhosted.org/packages/d5/cb/b9e93ebf193a0bb89dbcd4f73d7b0e6ecb7c1b6c016671950e25f041835e/coverage-7.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:96121edfa4c2dfdda409877ea8608dd01de816a4dc4a0523356067b305e4e17a", size = 211803, upload-time = "2025-03-30T20:36:25.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/1a/cdbfe9e1bb14d3afcaf6bb6e1b9ba76c72666e329cd06865bbd241efd652/coverage-7.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8af63b9afa1031c0ef05b217faa598f3069148eeee6bb24b79da9012423b82", size = 240561, upload-time = "2025-03-30T20:36:27.548Z" }, + { url = "https://files.pythonhosted.org/packages/59/04/57f1223f26ac018d7ce791bfa65b0c29282de3e041c1cd3ed430cfeac5a5/coverage-7.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89b1f4af0d4afe495cd4787a68e00f30f1d15939f550e869de90a86efa7e0814", size = 238488, upload-time = "2025-03-30T20:36:29.175Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b1/0f25516ae2a35e265868670384feebe64e7857d9cffeeb3887b0197e2ba2/coverage-7.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94ec0be97723ae72d63d3aa41961a0b9a6f5a53ff599813c324548d18e3b9e8c", size = 239589, upload-time = "2025-03-30T20:36:30.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a4/99d88baac0d1d5a46ceef2dd687aac08fffa8795e4c3e71b6f6c78e14482/coverage-7.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8a1d96e780bdb2d0cbb297325711701f7c0b6f89199a57f2049e90064c29f6bd", size = 239366, upload-time = "2025-03-30T20:36:32.563Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/1db89e135feb827a868ed15f8fc857160757f9cab140ffee21342c783ceb/coverage-7.8.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f1d8a2a57b47142b10374902777e798784abf400a004b14f1b0b9eaf1e528ba4", size = 237591, upload-time = "2025-03-30T20:36:34.721Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6d/ac4d6fdfd0e201bc82d1b08adfacb1e34b40d21a22cdd62cfaf3c1828566/coverage-7.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cf60dd2696b457b710dd40bf17ad269d5f5457b96442f7f85722bdb16fa6c899", size = 238572, upload-time = "2025-03-30T20:36:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/25/5e/917cbe617c230f7f1745b6a13e780a3a1cd1cf328dbcd0fd8d7ec52858cd/coverage-7.8.0-cp39-cp39-win32.whl", hash = "sha256:be945402e03de47ba1872cd5236395e0f4ad635526185a930735f66710e1bd3f", size = 213966, upload-time = "2025-03-30T20:36:38.551Z" }, + { url = "https://files.pythonhosted.org/packages/bd/93/72b434fe550135869f9ea88dd36068af19afce666db576e059e75177e813/coverage-7.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:90e7fbc6216ecaffa5a880cdc9c77b7418c1dcb166166b78dbc630d07f278cc3", size = 214852, upload-time = "2025-03-30T20:36:40.209Z" }, { url = "https://files.pythonhosted.org/packages/c4/f1/1da77bb4c920aa30e82fa9b6ea065da3467977c2e5e032e38e66f1c57ffd/coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd", size = 203443, upload-time = "2025-03-30T20:36:41.959Z" }, { url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435, upload-time = "2025-03-30T20:36:43.61Z" }, ] @@ -157,6 +203,18 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + [[package]] name = "ghp-import" version = "2.1.0" @@ -205,6 +263,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1f/d3/a760d1062e44587230aa65573c70edaad4ee8a0e60e193a3172b304d24d8/griffe-1.6.1-py3-none-any.whl", hash = "sha256:b0131670db16834f82383bcf4f788778853c9bf4dc7a1a2b708bb0808ca56a98", size = 128615, upload-time = "2025-03-18T15:18:43.57Z" }, ] +[[package]] +name = "harp-protocol" +version = "0.2.0a2" +source = { editable = "." } +dependencies = [ + { name = "pyserial" }, +] + +[package.dev-dependencies] +dev = [ + { name = "mkdocs" }, + { name = "mkdocs-codeinclude-plugin" }, + { name = "mkdocs-git-authors-plugin" }, + { name = "mkdocs-git-committers-plugin-2" }, + { name = "mkdocs-material" }, + { name = "mkdocs-monorepo-plugin" }, + { name = "mkdocstrings-python" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [{ name = "pyserial", specifier = ">=3.5" }] + +[package.metadata.requires-dev] +dev = [ + { name = "mkdocs", specifier = ">=1.6.1" }, + { name = "mkdocs-codeinclude-plugin", specifier = ">=0.2.1" }, + { name = "mkdocs-git-authors-plugin", specifier = ">=0.9.4" }, + { name = "mkdocs-git-committers-plugin-2", specifier = ">=2.5.0" }, + { name = "mkdocs-material", specifier = ">=9.6.9" }, + { name = "mkdocs-monorepo-plugin", specifier = ">=1.1.0" }, + { name = "mkdocstrings-python", specifier = ">=1.16.6" }, + { name = "pytest", specifier = ">=8.3.5" }, + { name = "pytest-cov", specifier = ">=6.1.1" }, + { name = "ruff", specifier = ">=0.11.0" }, +] + [[package]] name = "idna" version = "3.10" @@ -214,6 +311,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + [[package]] name = "iniconfig" version = "2.0.0" @@ -239,6 +348,9 @@ wheels = [ name = "markdown" version = "3.7" source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, +] sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086, upload-time = "2024-08-16T15:55:17.812Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349, upload-time = "2024-08-16T15:55:16.176Z" }, @@ -250,6 +362,16 @@ version = "3.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" }, { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, @@ -290,6 +412,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344, upload-time = "2024-10-18T15:21:43.721Z" }, + { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389, upload-time = "2024-10-18T15:21:44.666Z" }, + { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607, upload-time = "2024-10-18T15:21:45.452Z" }, + { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728, upload-time = "2024-10-18T15:21:46.295Z" }, + { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826, upload-time = "2024-10-18T15:21:47.134Z" }, + { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843, upload-time = "2024-10-18T15:21:48.334Z" }, + { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219, upload-time = "2024-10-18T15:21:49.587Z" }, + { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946, upload-time = "2024-10-18T15:21:50.441Z" }, + { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063, upload-time = "2024-10-18T15:21:51.385Z" }, + { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506, upload-time = "2024-10-18T15:21:52.974Z" }, ] [[package]] @@ -309,6 +441,7 @@ dependencies = [ { name = "click" }, { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "ghp-import" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "jinja2" }, { name = "markdown" }, { name = "markupsafe" }, @@ -357,6 +490,7 @@ name = "mkdocs-get-deps" version = "0.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "mergedeep" }, { name = "platformdirs" }, { name = "pyyaml" }, @@ -441,12 +575,14 @@ name = "mkdocstrings" version = "0.29.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "jinja2" }, { name = "markdown" }, { name = "markupsafe" }, { name = "mkdocs" }, { name = "mkdocs-autorefs" }, { name = "pymdown-extensions" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8e/4d/a9484dc5d926295bdf308f1f6c4f07fcc99735b970591edc414d401fcc91/mkdocstrings-0.29.0.tar.gz", hash = "sha256:3657be1384543ce0ee82112c3e521bbf48e41303aa0c229b9ffcccba057d922e", size = 1212185, upload-time = "2025-03-10T13:10:11.445Z" } wheels = [ @@ -461,6 +597,7 @@ dependencies = [ { name = "griffe" }, { name = "mkdocs-autorefs" }, { name = "mkdocstrings" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8e/e7/0691e34e807a8f5c28f0988fcfeeb584f0b569ce433bf341944f14bdb3ff/mkdocstrings_python-1.16.6.tar.gz", hash = "sha256:cefe0f0e17ab4a4611f01b0a2af75e4298664e0ff54feb83c91a485bfed82dc9", size = 201565, upload-time = "2025-03-18T15:34:24.371Z" } wheels = [ @@ -521,45 +658,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, ] -[[package]] -name = "pyharp" -version = "0.1.0" -source = { editable = "." } -dependencies = [ - { name = "pyserial" }, -] - -[package.dev-dependencies] -dev = [ - { name = "mkdocs" }, - { name = "mkdocs-codeinclude-plugin" }, - { name = "mkdocs-git-authors-plugin" }, - { name = "mkdocs-git-committers-plugin-2" }, - { name = "mkdocs-material" }, - { name = "mkdocs-monorepo-plugin" }, - { name = "mkdocstrings-python" }, - { name = "pytest" }, - { name = "pytest-cov" }, - { name = "ruff" }, -] - -[package.metadata] -requires-dist = [{ name = "pyserial", specifier = ">=3.5" }] - -[package.metadata.requires-dev] -dev = [ - { name = "mkdocs", specifier = ">=1.6.1" }, - { name = "mkdocs-codeinclude-plugin", specifier = ">=0.2.1" }, - { name = "mkdocs-git-authors-plugin", specifier = ">=0.9.4" }, - { name = "mkdocs-git-committers-plugin-2", specifier = ">=2.5.0" }, - { name = "mkdocs-material", specifier = ">=9.6.9" }, - { name = "mkdocs-monorepo-plugin", specifier = ">=1.1.0" }, - { name = "mkdocstrings-python", specifier = ">=1.16.6" }, - { name = "pytest", specifier = ">=8.3.5" }, - { name = "pytest-cov", specifier = ">=6.1.1" }, - { name = "ruff", specifier = ">=0.11.0" }, -] - [[package]] name = "pymdown-extensions" version = "10.14.3" @@ -588,9 +686,11 @@ version = "8.3.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } wheels = [ @@ -640,6 +740,15 @@ version = "6.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, @@ -667,6 +776,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, + { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777, upload-time = "2024-08-06T20:33:25.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318, upload-time = "2024-08-06T20:33:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891, upload-time = "2024-08-06T20:33:28.974Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614, upload-time = "2024-08-06T20:33:34.157Z" }, + { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360, upload-time = "2024-08-06T20:33:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006, upload-time = "2024-08-06T20:33:37.501Z" }, + { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577, upload-time = "2024-08-06T20:33:39.389Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593, upload-time = "2024-08-06T20:33:46.63Z" }, + { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312, upload-time = "2024-08-06T20:33:49.073Z" }, ] [[package]] @@ -787,6 +905,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, ] +[[package]] +name = "typing-extensions" +version = "4.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, +] + [[package]] name = "urllib3" version = "2.3.0" @@ -802,6 +929,9 @@ version = "6.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, @@ -811,6 +941,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/7223011bb760fce8ddc53416beb65b83a3ea6d7d13738dde75eeb2c89679/watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8", size = 96390, upload-time = "2024-11-01T14:06:49.325Z" }, + { url = "https://files.pythonhosted.org/packages/9c/62/d2b21bc4e706d3a9d467561f487c2938cbd881c69f3808c43ac1ec242391/watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a", size = 88386, upload-time = "2024-11-01T14:06:50.536Z" }, + { url = "https://files.pythonhosted.org/packages/ea/22/1c90b20eda9f4132e4603a26296108728a8bfe9584b006bd05dd94548853/watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c", size = 89017, upload-time = "2024-11-01T14:06:51.717Z" }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, + { url = "https://files.pythonhosted.org/packages/5b/79/69f2b0e8d3f2afd462029031baafb1b75d11bb62703f0e1022b2e54d49ee/watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa", size = 87903, upload-time = "2024-11-01T14:06:57.052Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2b/dc048dd71c2e5f0f7ebc04dd7912981ec45793a03c0dc462438e0591ba5d/watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e", size = 88381, upload-time = "2024-11-01T14:06:58.193Z" }, { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, @@ -822,3 +959,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, ] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] From 3cf6d125b6b5132dbf787915dda3ef7572c2b512 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Mon, 30 Jun 2025 10:45:21 +0100 Subject: [PATCH 125/267] Add "include markdown plugin" to docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- mkdocs.yml | 1 + pyproject.toml | 1 + uv.lock | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/mkdocs.yml b/mkdocs.yml index de3b1b3..245523a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -7,6 +7,7 @@ plugins: - autorefs - codeinclude - monorepo + - include-markdown - mkdocstrings: handlers: python: diff --git a/pyproject.toml b/pyproject.toml index 934a2a8..807c115 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ dev = [ "mkdocs-codeinclude-plugin>=0.2.1", "mkdocs-git-authors-plugin>=0.9.4", "mkdocs-git-committers-plugin-2>=2.5.0", + "mkdocs-include-markdown-plugin>=7.1.6", "mkdocs-material>=9.6.9", "mkdocs-monorepo-plugin>=1.1.0", "mkdocstrings-python>=1.16.6", diff --git a/uv.lock b/uv.lock index 26c81af..50bde1d 100644 --- a/uv.lock +++ b/uv.lock @@ -24,6 +24,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/37/fb6973edeb700f6e3d6ff222400602ab1830446c25c7b4676d8de93e65b8/backrefs-5.8-py39-none-any.whl", hash = "sha256:a66851e4533fb5b371aa0628e1fee1af05135616b86140c9d787a2ffdf4b8fdc", size = 380336, upload-time = "2025-02-25T16:53:29.858Z" }, ] +[[package]] +name = "bracex" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/9a/fec38644694abfaaeca2798b58e276a8e61de49e2e37494ace423395febc/bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7", size = 26642, upload-time = "2025-06-22T19:12:31.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508, upload-time = "2025-06-22T19:12:29.781Z" }, +] + [[package]] name = "certifi" version = "2025.1.31" @@ -277,6 +286,7 @@ dev = [ { name = "mkdocs-codeinclude-plugin" }, { name = "mkdocs-git-authors-plugin" }, { name = "mkdocs-git-committers-plugin-2" }, + { name = "mkdocs-include-markdown-plugin" }, { name = "mkdocs-material" }, { name = "mkdocs-monorepo-plugin" }, { name = "mkdocstrings-python" }, @@ -294,6 +304,7 @@ dev = [ { name = "mkdocs-codeinclude-plugin", specifier = ">=0.2.1" }, { name = "mkdocs-git-authors-plugin", specifier = ">=0.9.4" }, { name = "mkdocs-git-committers-plugin-2", specifier = ">=2.5.0" }, + { name = "mkdocs-include-markdown-plugin", specifier = ">=7.1.6" }, { name = "mkdocs-material", specifier = ">=9.6.9" }, { name = "mkdocs-monorepo-plugin", specifier = ">=1.1.0" }, { name = "mkdocstrings-python", specifier = ">=1.16.6" }, @@ -526,6 +537,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/f5/768590251839a148c188d64779b809bde0e78a306295c18fc29d7fc71ce1/mkdocs_git_committers_plugin_2-2.5.0-py3-none-any.whl", hash = "sha256:1778becf98ccdc5fac809ac7b62cf01d3c67d6e8432723dffbb823307d1193c4", size = 11788, upload-time = "2025-01-30T07:30:45.748Z" }, ] +[[package]] +name = "mkdocs-include-markdown-plugin" +version = "7.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mkdocs" }, + { name = "wcmatch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/17/988d97ac6849b196f54d45ca9c60ca894880c160a512785f03834704b3d9/mkdocs_include_markdown_plugin-7.1.6.tar.gz", hash = "sha256:a0753cb82704c10a287f1e789fc9848f82b6beb8749814b24b03dd9f67816677", size = 23391, upload-time = "2025-06-13T18:25:51.193Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/a1/6cf1667a05e5f468e1263fcf848772bca8cc9e358cd57ae19a01f92c9f6f/mkdocs_include_markdown_plugin-7.1.6-py3-none-any.whl", hash = "sha256:7975a593514887c18ecb68e11e35c074c5499cfa3e51b18cd16323862e1f7345", size = 27161, upload-time = "2025-06-13T18:25:49.847Z" }, +] + [[package]] name = "mkdocs-material" version = "9.6.9" @@ -960,6 +984,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, ] +[[package]] +name = "wcmatch" +version = "10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bracex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/3e/c0bdc27cf06f4e47680bd5803a07cb3dfd17de84cde92dd217dcb9e05253/wcmatch-10.1.tar.gz", hash = "sha256:f11f94208c8c8484a16f4f48638a85d771d9513f4ab3f37595978801cb9465af", size = 117421, upload-time = "2025-06-22T19:14:02.49Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/d8/0d1d2e9d3fabcf5d6840362adcf05f8cf3cd06a73358140c3a97189238ae/wcmatch-10.1-py3-none-any.whl", hash = "sha256:5848ace7dbb0476e5e55ab63c6bbd529745089343427caa5537f230cc01beb8a", size = 39854, upload-time = "2025-06-22T19:14:00.978Z" }, +] + [[package]] name = "zipp" version = "3.23.0" From c8af5a1d69dfd44a6d6d452359fba404a98ebf3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Mon, 30 Jun 2025 10:46:05 +0100 Subject: [PATCH 126/267] Update version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 807c115..cd83f38 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "harp-protocol" -version = "0.2.0a1" +version = "0.2.0a2" description = "Library for data acquisition and control of devices implementing the Harp protocol." authors = [{ name= "Hardware and Software Platform, Champalimaud Foundation", email="software@research.fchampalimaud.org"}] license = "MIT" From 01b42cf76312e9e650edb1654e7e66bd3d9d8ae1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Tue, 1 Jul 2025 14:32:26 +0100 Subject: [PATCH 127/267] Add dependency to handle dataclasses on documentation generation --- mkdocs.yml | 2 ++ pyproject.toml | 3 ++- uv.lock | 35 +++++++++++++++++++++++++++++++---- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 245523a..c18a114 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -16,6 +16,8 @@ plugins: show_root_heading: true show_submodules: true show_source: false + extensions: + - griffe_fieldz - git-committers: repository: fchampalimaud/pyharp branch: main diff --git a/pyproject.toml b/pyproject.toml index cd83f38..3fdaa18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,13 +18,14 @@ Documentation = "https://fchampalimaud.github.io/pyharp/" [dependency-groups] dev = [ + "griffe-fieldz>=0.2.1", "mkdocs>=1.6.1", "mkdocs-codeinclude-plugin>=0.2.1", "mkdocs-git-authors-plugin>=0.9.4", "mkdocs-git-committers-plugin-2>=2.5.0", "mkdocs-include-markdown-plugin>=7.1.6", "mkdocs-material>=9.6.9", - "mkdocs-monorepo-plugin>=1.1.0", + "mkdocs-monorepo-plugin>=1.1.2", "mkdocstrings-python>=1.16.6", "pytest>=8.3.5", "pytest-cov>=6.1.1", diff --git a/uv.lock b/uv.lock index 50bde1d..343c1ec 100644 --- a/uv.lock +++ b/uv.lock @@ -224,6 +224,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, ] +[[package]] +name = "fieldz" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/62/698c5cc2e7d4c8c89e63033e2e9d3c74902a1bf28782712eacb0653097ce/fieldz-0.1.2.tar.gz", hash = "sha256:0448ed5dacb13eaa49da0db786e87fae298fbd2652d26c510e5d7aea6b6bebf4", size = 17277, upload-time = "2025-06-30T18:06:40.881Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/8c/8958392cade27a272daf45d09a08473073dedeccad94b097dfeb898d969f/fieldz-0.1.2-py3-none-any.whl", hash = "sha256:e25884d2821a2d5638ef8d4d8bce5d1039359cfcb46d0f93df8cb1f7c2eb3a2e", size = 17878, upload-time = "2025-06-30T18:06:39.322Z" }, +] + [[package]] name = "ghp-import" version = "2.1.0" @@ -272,6 +284,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1f/d3/a760d1062e44587230aa65573c70edaad4ee8a0e60e193a3172b304d24d8/griffe-1.6.1-py3-none-any.whl", hash = "sha256:b0131670db16834f82383bcf4f788778853c9bf4dc7a1a2b708bb0808ca56a98", size = 128615, upload-time = "2025-03-18T15:18:43.57Z" }, ] +[[package]] +name = "griffe-fieldz" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fieldz" }, + { name = "griffe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/95/5c47b6c54d106c76aad9b51177401ad4215a0de910c6853b689a2c6d07da/griffe_fieldz-0.2.1.tar.gz", hash = "sha256:7371693cdd045ac95aaebd16a81586d8dfa72c30e2d4519e0dde0d80f8ef0dc7", size = 8148, upload-time = "2025-01-12T20:55:09.063Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/fc/c059d128d77a369f54a3753162a38cd7668e5b4d14464672001a50cf2e9d/griffe_fieldz-0.2.1-py3-none-any.whl", hash = "sha256:04ae78b487c832a38b0495f971784d513da413b867c51e429f39d74f76d4f941", size = 5691, upload-time = "2025-01-12T20:55:07.634Z" }, +] + [[package]] name = "harp-protocol" version = "0.2.0a2" @@ -282,6 +307,7 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "griffe-fieldz" }, { name = "mkdocs" }, { name = "mkdocs-codeinclude-plugin" }, { name = "mkdocs-git-authors-plugin" }, @@ -300,13 +326,14 @@ requires-dist = [{ name = "pyserial", specifier = ">=3.5" }] [package.metadata.requires-dev] dev = [ + { name = "griffe-fieldz", specifier = ">=0.2.1" }, { name = "mkdocs", specifier = ">=1.6.1" }, { name = "mkdocs-codeinclude-plugin", specifier = ">=0.2.1" }, { name = "mkdocs-git-authors-plugin", specifier = ">=0.9.4" }, { name = "mkdocs-git-committers-plugin-2", specifier = ">=2.5.0" }, { name = "mkdocs-include-markdown-plugin", specifier = ">=7.1.6" }, { name = "mkdocs-material", specifier = ">=9.6.9" }, - { name = "mkdocs-monorepo-plugin", specifier = ">=1.1.0" }, + { name = "mkdocs-monorepo-plugin", specifier = ">=1.1.2" }, { name = "mkdocstrings-python", specifier = ">=1.16.6" }, { name = "pytest", specifier = ">=8.3.5" }, { name = "pytest-cov", specifier = ">=6.1.1" }, @@ -583,15 +610,15 @@ wheels = [ [[package]] name = "mkdocs-monorepo-plugin" -version = "1.1.0" +version = "1.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mkdocs" }, { name = "python-slugify" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f7/6c/5b2a34fd63fe20724e2edf1879e977b40453efe40e1c385a05f38b420664/mkdocs-monorepo-plugin-1.1.0.tar.gz", hash = "sha256:ccc566e166aac5ae7fade498c15c4a337a4892d238629b51aba8ef3fc7099034", size = 13435, upload-time = "2024-01-04T14:29:15.892Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/6a/a75245020e44beb9d7c806158f8a2cda37597711409d40c5a37c70078a7e/mkdocs-monorepo-plugin-1.1.2.tar.gz", hash = "sha256:09200bcf837ad35070e6da973aa0cb682e69ed6e16f254a30584550c6d2d8ebb", size = 13723, upload-time = "2025-06-05T19:09:45.042Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/33/4cc6c70223aee511244f8fe7706df70d1cd253d1446ab466c73f9dfbaab5/mkdocs_monorepo_plugin-1.1.0-py3-none-any.whl", hash = "sha256:7bbfd9756a7fdecf64d6105dad96cce7e7bb5f0d6cfc2bfda31a1919c77cc3b9", size = 14312, upload-time = "2024-01-04T14:29:14.09Z" }, + { url = "https://files.pythonhosted.org/packages/57/26/4f4c19457d1d4e6d571a3b092921b7a0ce9477d18d997755ac615d72b96b/mkdocs_monorepo_plugin-1.1.2-py3-none-any.whl", hash = "sha256:4b917bc224b89e34e1736bb31ad5ae9deb0a907da879e03bb9454b41fb8b1cac", size = 14539, upload-time = "2025-06-05T19:09:43.74Z" }, ] [[package]] From 7ec55183d63fe9370e408ab0c20d1d2a158592bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Tue, 1 Jul 2025 14:33:37 +0100 Subject: [PATCH 128/267] Update mkdocs config - Removed emojis - Added footer next/prev navigation - Added copyright to footer - Added github link to footer --- mkdocs.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index c18a114..97b0140 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,6 +1,7 @@ site_name: pyharp repo_url: "https://github.com/fchampalimaud/pyharp" # repo_name: "pyharp" +copyright: Copyright © 2025, Hardware and Software Platform, Champalimaud Foundation plugins: - search @@ -29,9 +30,6 @@ markdown_extensions: - attr_list - admonition - pymdownx.details - - pymdownx.emoji: - emoji_index: !!python/name:material.extensions.emoji.twemoji - emoji_generator: !!python/name:material.extensions.emoji.to_svg - pymdownx.highlight: anchor_linenums: true line_spans: __span @@ -55,6 +53,7 @@ theme: # - navigation.sections - navigation.indexes - navigation.expand + - navigation.footer palette: - media: "(prefers-color-scheme)" toggle: @@ -87,7 +86,12 @@ nav: - Protocol: api/protocol.md - Device: api/device.md - Messages: api/messages.md - - Devices: '*include ./harp.devices/*/mkdocs.yml' + - Devices: '*include ../harp.devices/*/mkdocs.yml' + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/fchampalimaud/pyharp extra_css: - stylesheets/extra.css From a97fd0a80244e41a2238fc16bab9b9b37ffb9f47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 2 Jul 2025 10:16:35 +0100 Subject: [PATCH 129/267] Update docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- harp/communication/device.py | 186 +++++++++++++++--------------- harp/communication/harp_serial.py | 10 +- harp/protocol/base.py | 160 ++++++++++++------------- harp/protocol/exceptions.py | 8 ++ harp/protocol/messages.py | 62 +++++----- 5 files changed, 217 insertions(+), 209 deletions(-) diff --git a/harp/communication/device.py b/harp/communication/device.py index 5647293..6662c62 100644 --- a/harp/communication/device.py +++ b/harp/communication/device.py @@ -29,27 +29,27 @@ class Device: Attributes ---------- WHO_AM_I : int - the device ID number. A list of devices can be found [here](https://github.com/harp-tech/protocol/blob/main/whoami.md) + The device ID number. A list of devices can be found [here](https://github.com/harp-tech/protocol/blob/main/whoami.md) DEFAULT_DEVICE_NAME : str - the device name, i.e. "Behavior". This name is derived by cross-referencing the `WHO_AM_I` identifier with the corresponding device name in the `device_names` dictionary + The device name, i.e. "Behavior". This name is derived by cross-referencing the `WHO_AM_I` identifier with the corresponding device name in the `device_names` dictionary HW_VERSION_H : int - the major hardware version + The major hardware version HW_VERSION_L : int - the minor hardware version + The minor hardware version ASSEMBLY_VERSION : int - the version of the assembled components + The version of the assembled components HARP_VERSION_H : int - the major Harp core version + The major Harp core version HARP_VERSION_L : int - the minor Harp core version + The minor Harp core version FIRMWARE_VERSION_H : int - the major firmware version + The major firmware version FIRMWARE_VERSION_L : int - the minor firmware version + The minor firmware version DEVICE_NAME : str - the device name stored in the Harp device + The device name stored in the Harp device SERIAL_NUMBER : int, optional - the serial number of the device + The serial number of the device """ WHO_AM_I: int @@ -83,9 +83,9 @@ def __init__( Parameters ---------- serial_port : str - the serial port used to establish the connection with the Harp device. It must be denoted as `/dev/ttyUSBx` in Linux and `COMx` in Windows, where `x` is the number of the serial port + The serial port used to establish the connection with the Harp device. It must be denoted as `/dev/ttyUSBx` in Linux and `COMx` in Windows, where `x` is the number of the serial port dump_file_path: str, optional - the binary file to which all Harp messages will be written + The binary file to which all Harp messages will be written read_timeout_s: float, optional _TODO_ """ @@ -170,7 +170,7 @@ def _read_device_mode(self) -> OperationMode: Returns ------- DeviceMode - the current device mode + The current device mode """ address = CommonRegisters.OPERATION_CTRL reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8)) @@ -184,7 +184,7 @@ def dump_registers(self) -> list: Returns ------- list - the list containing the reply Harp messages for all the device's registers + The list containing the reply Harp messages for all the device's registers """ address = CommonRegisters.OPERATION_CTRL reg_value = self.send( @@ -213,12 +213,12 @@ def set_mode(self, mode: OperationMode) -> ReplyHarpMessage: Parameters ---------- mode : DeviceMode - the new device mode value + The new device mode value Returns ------- ReplyHarpMessage - the reply to the Harp message + The reply to the Harp message """ address = CommonRegisters.OPERATION_CTRL @@ -245,12 +245,12 @@ def alive_en(self, enable: bool) -> bool: Parameters ---------- enable : bool - If True, enables the ALIVE_EN bit. If False, disables it. + If True, enables the ALIVE_EN bit. If False, disables it Returns ------- bool - True if the operation was successful, False otherwise. + True if the operation was successful, False otherwise """ address = CommonRegisters.OPERATION_CTRL @@ -277,12 +277,12 @@ def op_led_en(self, enable: bool) -> bool: Parameters ---------- enable : bool - If True, enables the operation LED. If False, disables it. + If True, enables the operation LED. If False, disables it Returns ------- bool - True if the operation was successful, False otherwise. + True if the operation was successful, False otherwise """ address = CommonRegisters.OPERATION_CTRL @@ -309,12 +309,12 @@ def status_led(self, enable: bool) -> bool: Parameters ---------- enable : bool - If True, enables the status led. If False, disables it. + If True, enables the status led. If False, disables it Returns ------- bool - True if the operation was successful, False otherwise. + True if the operation was successful, False otherwise """ address = CommonRegisters.OPERATION_CTRL @@ -341,12 +341,12 @@ def mute_reply(self, enable: bool) -> bool: Parameters ---------- enable : bool - If True, the Replies to all the Commands are muted. If False, un-mutes them. + If True, the Replies to all the Commands are muted. If False, un-mutes them Returns ------- bool - True if the operation was successful, False otherwise. + True if the operation was successful, False otherwise """ address = CommonRegisters.OPERATION_CTRL @@ -375,7 +375,7 @@ def reset_device( Returns ------- ReplyHarpMessage - the reply to the Harp message + The reply to the Harp message """ address = CommonRegisters.RESET_DEV reply = self.send( @@ -391,12 +391,12 @@ def set_clock_config(self, clock_config: ClockConfig) -> ReplyHarpMessage: Parameters ---------- clock_config : ClockConfig - the clock configuration value + The clock configuration value Returns ------- ReplyHarpMessage - the reply to the Harp message + The reply to the Harp message """ address = CommonRegisters.CLOCK_CONFIG reply = self.send( @@ -412,12 +412,12 @@ def set_timestamp_offset(self, timestamp_offset: int) -> ReplyHarpMessage: Parameters ---------- timestamp_offset : int - the timestamp offset value + The timestamp offset value Returns ------- ReplyHarpMessage - the reply to the Harp message + The reply to the Harp message """ address = CommonRegisters.TIMESTAMP_OFFSET reply = self.send( @@ -435,12 +435,12 @@ def send(self, message: HarpMessage) -> Optional[ReplyHarpMessage]: Parameters ---------- message : HarpMessage - the HarpMessage containing the message to be sent to the device + The HarpMessage containing the message to be sent to the device Returns ------- Optional[ReplyHarpMessage] - the reply to the Harp message or None if no reply is given + The reply to the Harp message or None if no reply is given """ self._ser.write(message.frame) @@ -459,7 +459,7 @@ def _read(self) -> Union[ReplyHarpMessage, None]: Returns ------- Union[ReplyHarpMessage, None] - the incoming Harp message in case it exists + The incoming Harp message in case it exists """ try: return self._ser.msg_q.get(block=True, timeout=self._read_timeout_s) @@ -480,7 +480,7 @@ def get_events(self) -> list[ReplyHarpMessage]: Returns ------- list - the list containing every Harp event message that were on the queue + The list containing every Harp event message that were on the queue """ msgs = [] while True: @@ -497,7 +497,7 @@ def event_count(self) -> int: Returns ------- int - the number of events in the event queue + The number of events in the event queue """ return self._ser.event_q.qsize() @@ -508,12 +508,12 @@ def read_u8(self, address: int) -> ReplyHarpMessage: Parameters ---------- address : int - the register to be read + The register to be read Returns ------- ReplyHarpMessage - the reply to the Harp message that will contain the value read from the register + The reply to the Harp message that will contain the value read from the register """ return self.send( HarpMessage.create( @@ -530,12 +530,12 @@ def read_s8(self, address: int) -> ReplyHarpMessage: Parameters ---------- address : int - the register to be read + The register to be read Returns ------- ReplyHarpMessage - the reply to the Harp message that will contain the value read from the register + The reply to the Harp message that will contain the value read from the register """ return self.send( HarpMessage.create( @@ -552,12 +552,12 @@ def read_u16(self, address: int) -> ReplyHarpMessage: Parameters ---------- address : int - the register to be read + The register to be read Returns ------- ReplyHarpMessage - the reply to the Harp message that will contain the value read from the register + The reply to the Harp message that will contain the value read from the register """ return self.send( HarpMessage.create( @@ -574,12 +574,12 @@ def read_s16(self, address: int) -> ReplyHarpMessage: Parameters ---------- address : int - the register to be read + The register to be read Returns ------- ReplyHarpMessage - the reply to the Harp message that will contain the value read from the register + The reply to the Harp message that will contain the value read from the register """ return self.send( HarpMessage.create( @@ -596,12 +596,12 @@ def read_u32(self, address: int) -> ReplyHarpMessage: Parameters ---------- address : int - the register to be read + The register to be read Returns ------- ReplyHarpMessage - the reply to the Harp message that will contain the value read from the register + The reply to the Harp message that will contain the value read from the register """ return self.send( HarpMessage.create( @@ -618,12 +618,12 @@ def read_s32(self, address: int) -> ReplyHarpMessage: Parameters ---------- address : int - the register to be read + The register to be read Returns ------- ReplyHarpMessage - the reply to the Harp message that will contain the value read from the register + The reply to the Harp message that will contain the value read from the register """ return self.send( HarpMessage.create( @@ -640,12 +640,12 @@ def read_u64(self, address: int) -> ReplyHarpMessage: Parameters ---------- address : int - the register to be read + The register to be read Returns ------- ReplyHarpMessage - the reply to the Harp message that will contain the value read from the register + The reply to the Harp message that will contain the value read from the register """ return self.send( HarpMessage.create( @@ -662,12 +662,12 @@ def read_s64(self, address: int) -> ReplyHarpMessage: Parameters ---------- address : int - the register to be read + The register to be read Returns ------- ReplyHarpMessage - the reply to the Harp message that will contain the value read from the register + The reply to the Harp message that will contain the value read from the register """ return self.send( HarpMessage.create( @@ -684,12 +684,12 @@ def read_float(self, address: int) -> ReplyHarpMessage: Parameters ---------- address : int - the register to be read + The register to be read Returns ------- ReplyHarpMessage - the reply to the Harp message that will contain the value read from the register + The reply to the Harp message that will contain the value read from the register """ return self.send( HarpMessage.create( @@ -706,14 +706,14 @@ def write_u8(self, address: int, value: int | list[int]) -> ReplyHarpMessage: Parameters ---------- address : int - the register to be written on + The register to be written on value: int | list[int] - the value to be written to the register + The value to be written to the register Returns ------- ReplyHarpMessage - the reply to the Harp message + The reply to the Harp message """ return self.send( HarpMessage.create( @@ -731,14 +731,14 @@ def write_s8(self, address: int, value: int | list[int]) -> ReplyHarpMessage: Parameters ---------- address : int - the register to be written on + The register to be written on value: int | list[int] - the value to be written to the register + The value to be written to the register Returns ------- ReplyHarpMessage - the reply to the Harp message + The reply to the Harp message """ return self.send( HarpMessage.create( @@ -756,14 +756,14 @@ def write_u16(self, address: int, value: int | list[int]) -> ReplyHarpMessage: Parameters ---------- address : int - the register to be written on + The register to be written on value: int | list[int] - the value to be written to the register + The value to be written to the register Returns ------- ReplyHarpMessage - the reply to the Harp message + The reply to the Harp message """ return self.send( HarpMessage.create( @@ -781,14 +781,14 @@ def write_s16(self, address: int, value: int | list[int]) -> ReplyHarpMessage: Parameters ---------- address : int - the register to be written on + The register to be written on value: int | list[int] - the value to be written to the register + The value to be written to the register Returns ------- ReplyHarpMessage - the reply to the Harp message + The reply to the Harp message """ return self.send( HarpMessage.create( @@ -806,14 +806,14 @@ def write_u32(self, address: int, value: int | list[int]) -> ReplyHarpMessage: Parameters ---------- address : int - the register to be written on + The register to be written on value: int | list[int] - the value to be written to the register + The value to be written to the register Returns ------- ReplyHarpMessage - the reply to the Harp message + The reply to the Harp message """ return self.send( HarpMessage.create( @@ -831,14 +831,14 @@ def write_s32(self, address: int, value: int | list[int]) -> ReplyHarpMessage: Parameters ---------- address : int - the register to be written on + The register to be written on value: int | list[int] - the value to be written to the register + The value to be written to the register Returns ------- ReplyHarpMessage - the reply to the Harp message + The reply to the Harp message """ return self.send( HarpMessage.create( @@ -856,14 +856,14 @@ def write_u64(self, address: int, value: int | list[int]) -> ReplyHarpMessage: Parameters ---------- address : int - the register to be written on + The register to be written on value: int | list[int] - the value to be written to the register + The value to be written to the register Returns ------- ReplyHarpMessage - the reply to the Harp message + The reply to the Harp message """ return self.send( HarpMessage.create( @@ -881,14 +881,14 @@ def write_s64(self, address: int, value: int | list[int]) -> ReplyHarpMessage: Parameters ---------- address : int - the register to be written on + The register to be written on value: int | list[int] - the value to be written to the register + The value to be written to the register Returns ------- ReplyHarpMessage - the reply to the Harp message + The reply to the Harp message """ return self.send( HarpMessage.create( @@ -906,14 +906,14 @@ def write_float(self, address: int, value: float | list[float]) -> ReplyHarpMess Parameters ---------- address : int - the register to be written on + The register to be written on value: int | list[int] - the value to be written to the register + The value to be written to the register Returns ------- ReplyHarpMessage - the reply to the Harp message + The reply to the Harp message """ return self.send( HarpMessage.create( @@ -931,7 +931,7 @@ def _read_who_am_i(self) -> int: Returns ------- int - the value of the `WHO_AM_I` register. + The value of the `WHO_AM_I` register """ address = CommonRegisters.WHO_AM_I @@ -948,7 +948,7 @@ def _read_default_device_name(self) -> str: Returns ------- str - the default device name. + The default device name """ return device_names.get(self.WHO_AM_I, "Unknown device") @@ -959,7 +959,7 @@ def _read_hw_version_h(self) -> int: Returns ------- int - the value of the `HW_VERSION_H` register. + The value of the `HW_VERSION_H` register """ address = CommonRegisters.HW_VERSION_H @@ -976,7 +976,7 @@ def _read_hw_version_l(self) -> int: Returns ------- int - the value of the `HW_VERSION_L` register. + The value of the `HW_VERSION_L` register """ address = CommonRegisters.HW_VERSION_L @@ -993,7 +993,7 @@ def _read_assembly_version(self) -> int: Returns ------- int - the value of the `ASSEMBLY_VERSION` register. + The value of the `ASSEMBLY_VERSION` register """ address = CommonRegisters.ASSEMBLY_VERSION @@ -1010,7 +1010,7 @@ def _read_harp_version_h(self) -> int: Returns ------- int - the value of the `HARP_VERSION_H` register. + The value of the `HARP_VERSION_H` register """ address = CommonRegisters.HARP_VERSION_H @@ -1027,7 +1027,7 @@ def _read_harp_version_l(self) -> int: Returns ------- int - the value of the `HARP_VERSION_L` register. + The value of the `HARP_VERSION_L` register """ address = CommonRegisters.HARP_VERSION_L @@ -1044,7 +1044,7 @@ def _read_fw_version_h(self) -> int: Returns ------- int - the value of the `FW_VERSION_H` register. + The value of the `FW_VERSION_H` register """ address = CommonRegisters.FIRMWARE_VERSION_H @@ -1061,7 +1061,7 @@ def _read_fw_version_l(self) -> int: Returns ------- int - the value of the `FW_VERSION_L` register. + The value of the `FW_VERSION_L` register """ address = CommonRegisters.FIRMWARE_VERSION_L @@ -1078,7 +1078,7 @@ def _read_device_name(self) -> str: Returns ------- int - the value of the `DEVICE_NAME` register. + The value of the `DEVICE_NAME` register """ address = CommonRegisters.DEVICE_NAME @@ -1095,7 +1095,7 @@ def _read_serial_number(self) -> int: Returns ------- int - the value of the `SERIAL_NUMBER` register. + The value of the `SERIAL_NUMBER` register """ address = CommonRegisters.SERIAL_NUMBER @@ -1115,7 +1115,7 @@ def _read_clock_config(self) -> int: Returns ------- int - the value of the `CLOCK_CONFIG` register. + The value of the `CLOCK_CONFIG` register """ address = CommonRegisters.CLOCK_CONFIG @@ -1132,7 +1132,7 @@ def _read_timestamp_offset(self) -> int: Returns ------- int - the value of the `TIMESTAMP_OFFSET` register. + The value of the `TIMESTAMP_OFFSET` register """ address = CommonRegisters.TIMESTAMP_OFFSET diff --git a/harp/communication/harp_serial.py b/harp/communication/harp_serial.py index d5d5578..c966d79 100644 --- a/harp/communication/harp_serial.py +++ b/harp/communication/harp_serial.py @@ -22,7 +22,7 @@ def __init__(self, read_q: queue.Queue, *args, **kwargs): Parameters ---------- read_q : queue.Queue - the queue to where the data received will be put + The queue to where the data received will be put """ self._read_q = read_q self._buffer = bytearray() @@ -46,7 +46,7 @@ def data_received(self, data: bytes) -> None: Parameters ---------- data : bytes - the data received from the serial communication + The data received from the serial communication """ self._buffer.extend(data) while True: @@ -83,9 +83,9 @@ class HarpSerial: Attributes ---------- msg_q : queue.Queue - the queue containing the Harp messages that are not of the type `MessageType.EVENT` + The queue containing the Harp messages that are not of the type `MessageType.EVENT` event_q : queue.Queue - the queue containing the Harp messages of `MessageType.EVENT` + The queue containing the Harp messages of `MessageType.EVENT` """ msg_q: queue.Queue @@ -96,7 +96,7 @@ def __init__(self, serial_port: str, **kwargs): Parameters ---------- serial_port : str - the serial port used to establish the connection with the Harp device. It must be denoted as `/dev/ttyUSBx` in Linux and `COMx` in Windows, where `x` is the number of the serial port + The serial port used to establish the connection with the Harp device. It must be denoted as `/dev/ttyUSBx` in Linux and `COMx` in Windows, where `x` is the number of the serial port """ # Connect to the Harp device self._ser = serial.Serial(serial_port, **kwargs) diff --git a/harp/protocol/base.py b/harp/protocol/base.py index fe3ad59..a634f52 100644 --- a/harp/protocol/base.py +++ b/harp/protocol/base.py @@ -14,15 +14,15 @@ class MessageType(IntEnum): Attributes ---------- READ : int - the value that corresponds to a Read Harp message (1) + The value that corresponds to a Read Harp message (1) WRITE : int - the value that corresponds to a Write Harp message (2) + The value that corresponds to a Write Harp message (2) EVENT : int - the value that corresponds to an Event Harp message (3). Messages of this type are only meant to be send by the device + The value that corresponds to an Event Harp message (3). Messages of this type are only meant to be send by the device READ_ERROR : int - the value that corresponds to a Read Error Harp message (9). Messages of this type are only meant to be send by the device + The value that corresponds to a Read Error Harp message (9). Messages of this type are only meant to be send by the device WRITE_ERROR : int - the value that corresponds to a Write Error Harp message (10). Messages of this type are only meant to be send by the device + The value that corresponds to a Write Error Harp message (10). Messages of this type are only meant to be send by the device """ READ: int = 1 @@ -38,44 +38,44 @@ class PayloadType(IntEnum): Attributes ---------- - U8 : PayloadType - the value that corresponds to a message of type U8 - S8 : PayloadType - the value that corresponds to a message of type S8 - U16 : PayloadType - the value that corresponds to a message of type U16 - S16 : PayloadType - the value that corresponds to a message of type S16 - U32 : PayloadType - the value that corresponds to a message of type U32 - S32 : PayloadType - the value that corresponds to a message of type S32 - U64 : PayloadType - the value that corresponds to a message of type U64 - S64 : PayloadType - the value that corresponds to a message of type S64 - Float : PayloadType - the value that corresponds to a message of type Float - Timestamp: PayloadType - the value that corresponds to a message of type Timestamp. This is not a valid PayloadType, but it is used to indicate that the message has a timestamp. - TimestampedU8 : PayloadType - the value that corresponds to a message of type TimestampedU8 - TimestampedS8 : PayloadType - the value that corresponds to a message of type TimestampedS8 - TimestampedU16 : PayloadType - the value that corresponds to a message of type TimestampedU16 - TimestampedS16 : PayloadType - the value that corresponds to a message of type TimestampedS16 - TimestampedU32 : PayloadType - the value that corresponds to a message of type TimestampedU32 - TimestampedS32 : PayloadType - the value that corresponds to a message of type TimestampedS32 - TimestampedU64 : PayloadType - the value that corresponds to a message of type TimestampedU64 - TimestampedS64 : PayloadType - the value that corresponds to a message of type TimestampedS64 - TimestampedFloat : PayloadType - the value that corresponds to a message of type TimestampedFloat + U8 : int + The value that corresponds to a message of type U8 + S8 : int + The value that corresponds to a message of type S8 + U16 : int + The value that corresponds to a message of type U16 + S16 : int + The value that corresponds to a message of type S16 + U32 : int + The value that corresponds to a message of type U32 + S32 : int + The value that corresponds to a message of type S32 + U64 : int + The value that corresponds to a message of type U64 + S64 : int + The value that corresponds to a message of type S64 + Float : int + The value that corresponds to a message of type Float + Timestamp: int + The value that corresponds to a message of type Timestamp. This is not a valid PayloadType, but it is used to indicate that the message has a timestamp. + TimestampedU8 : int + The value that corresponds to a message of type TimestampedU8 + TimestampedS8 : int + The value that corresponds to a message of type TimestampedS8 + TimestampedU16 : int + The value that corresponds to a message of type TimestampedU16 + TimestampedS16 : int + The value that corresponds to a message of type TimestampedS16 + TimestampedU32 : int + The value that corresponds to a message of type TimestampedU32 + TimestampedS32 : int + The value that corresponds to a message of type TimestampedS32 + TimestampedU64 : int + The value that corresponds to a message of type TimestampedU64 + TimestampedS64 : int + The value that corresponds to a message of type TimestampedS64 + TimestampedFloat : int + The value that corresponds to a message of type TimestampedFloat """ U8 = _isUnsigned | 1 @@ -106,37 +106,37 @@ class CommonRegisters(IntEnum): Attributes ---------- WHO_AM_I : int - the number of the `WHO_AM_I` register + The number of the `WHO_AM_I` register HW_VERSION_H : int - the number of the `HW_VERSION_H` register + The number of the `HW_VERSION_H` register HW_VERSION_L : int - the number of the `HW_VERSION_L` register + The number of the `HW_VERSION_L` register ASSEMBLY_VERSION : int - the number of the `ASSEMBLY_VERSION` register + The number of the `ASSEMBLY_VERSION` register HARP_VERSION_H : int - the number of the `HARP_VERSION_H` register + The number of the `HARP_VERSION_H` register HARP_VERSION_L : int - the number of the `HARP_VERSION_L` register + The number of the `HARP_VERSION_L` register FIRMWARE_VERSION_H : int - the number of the `FIRMWARE_VERSION_H` register + The number of the `FIRMWARE_VERSION_H` register FIRMWARE_VERSION_L : int - the number of the `FIRMWARE_VERSION_L` register + The number of the `FIRMWARE_VERSION_L` register TIMESTAMP_SECOND : int - the number of the `TIMESTAMP_SECOND` register + The number of the `TIMESTAMP_SECOND` register TIMESTAMP_MICRO : int - the number of the `TIMESTAMP_MICRO` register + The number of the `TIMESTAMP_MICRO` register OPERATION_CTRL : int - the number of the `OPERATION_CTRL` register + The number of the `OPERATION_CTRL` register RESET_DEV : int - the number of the `RESET_DEV` register + The number of the `RESET_DEV` register DEVICE_NAME : int - the number of the `DEVICE_NAME` register + The number of the `DEVICE_NAME` register SERIAL_NUMBER : int - the number of the `SERIAL_NUMBER` register + The number of the `SERIAL_NUMBER` register CLOCK_CONFIG : int - the number of the `CLOCK_CONFIG` register + The number of the `CLOCK_CONFIG` register TIMESTAMP_OFFSET : int - the number of the `TIMESTAMP_OFFSET` register + The number of the `TIMESTAMP_OFFSET` register """ WHO_AM_I = 0x00 @@ -164,13 +164,13 @@ class OperationMode(IntEnum): Attributes ---------- STANDBY : int - the value that corresponds to the Standby operation mode (0). The device has all the Events turned off. + The value that corresponds to the Standby operation mode (0). The device has all the Events turned off ACTIVE : int - the value that corresponds to the Active operation mode (1). The device turns ON the Events detection. Only the enabled Events will be operating. + The value that corresponds to the Active operation mode (1). The device turns ON the Events detection. Only the enabled Events will be operating RESERVED : int - the value that corresponds to the Reserved operation mode (2) + The value that corresponds to the Reserved operation mode (2) SPEED : int - the value that corresponds to the Speed operation mode (3). The device enters Speed Mode. + The value that corresponds to the Speed operation mode (3). The device enters Speed Mode """ STANDBY = 0 @@ -192,15 +192,15 @@ class OperationCtrl(IntFlag): 2: Reserved 3: Speed Mode (device enters Speed Mode, optional; only responds to Speed Mode commands) DUMP : int - Bit 3 (0x08): When set to 1, the device adds the content of all registers to the streaming buffer as Read messages. Always read as 0. + Bit 3 (0x08): When set to 1, the device adds the content of all registers to the streaming buffer as Read messages. Always read as 0 MUTE_RPL : int - Bit 4 (0x10): If set to 1, replies to all commands are muted (not sent by the device). + Bit 4 (0x10): If set to 1, replies to all commands are muted (not sent by the device) VISUALEN : int - Bit 5 (0x20): If set to 1, visual indications (e.g., LEDs) operate. If 0, all visual indications are turned off. + Bit 5 (0x20): If set to 1, visual indications (e.g., LEDs) operate. If 0, all visual indications are turned off OPLEDEN : int - Bit 6 (0x40): If set to 1, the LED indicates the selected Operation Mode (see LED feedback table in documentation). + Bit 6 (0x40): If set to 1, the LED indicates the selected Operation Mode (see LED feedback table in documentation) ALIVE_EN : int - Bit 7 (0x80): If set to 1, the device sends an Event Message with the R_TIMESTAMP_SECONDS content each second (heartbeat). + Bit 7 (0x80): If set to 1, the device sends an Event Message with the R_TIMESTAMP_SECONDS content each second (heartbeat) """ OP_MODE = 3 << 0 @@ -220,19 +220,19 @@ class ResetMode(IntEnum): ---------- RST_DEF : int Bit 0 (0x01): If set, resets the device and restores all registers (Common and Application) to default values. - EEPROM is erased and defaults become the permanent boot option. + EEPROM is erased and defaults become the permanent boot option RST_EE : int Bit 1 (0x02): If set, resets the device and restores all registers (Common and Application) from non-volatile memory (EEPROM). - EEPROM values remain the permanent boot option. + EEPROM values remain the permanent boot option SAVE : int Bit 3 (0x08): If set, saves all non-volatile registers (Common and Application) to EEPROM and reboots. - EEPROM becomes the permanent boot option. + EEPROM becomes the permanent boot option NAME_TO_DEFAULT : int - Bit 4 (0x10): If set, reboots the device with the default name. + Bit 4 (0x10): If set, reboots the device with the default name BOOT_DEF : int - Bit 6 (0x40, read-only): Indicates the device booted with default register values. + Bit 6 (0x40, read-only): Indicates the device booted with default register values BOOT_EE : int - Bit 7 (0x80, read-only): Indicates the device booted with register values saved on the EEPROM. + Bit 7 (0x80, read-only): Indicates the device booted with register values saved on the EEPROM """ RST_DEF = 0x01 @@ -252,20 +252,20 @@ class ClockConfig(IntFlag): ---------- CLK_REP : int Bit 0 (0x01): If set to 1, the device will repeat the Harp Synchronization Clock to the Clock Output connector, if available. - Acts as a daisy-chain by repeating the Clock Input to the Clock Output. Setting this bit also unlocks the Harp Synchronization Clock. + Acts as a daisy-chain by repeating the Clock Input to the Clock Output. Setting this bit also unlocks the Harp Synchronization Clock CLK_GEN : int Bit 1 (0x02): If set to 1, the device will generate Harp Synchronization Clock to the Clock Output connector, if available. - The Clock Input will be ignored. Read as 1 if the device is generating the Harp Synchronization Clock. + The Clock Input will be ignored. Read as 1 if the device is generating the Harp Synchronization Clock REP_ABLE : int - Bit 3 (0x08, read-only): Indicates if the device is able (1) to repeat the Harp Synchronization Clock timestamp. + Bit 3 (0x08, read-only): Indicates if the device is able (1) to repeat the Harp Synchronization Clock timestamp GEN_ABLE : int - Bit 4 (0x10, read-only): Indicates if the device is able (1) to generate the Harp Synchronization Clock timestamp. + Bit 4 (0x10, read-only): Indicates if the device is able (1) to generate the Harp Synchronization Clock timestamp CLK_UNLOCK : int Bit 6 (0x40): If set to 1, the device will unlock the timestamp register counter (R_TIMESTAMP_SECOND) and accept new timestamp values. - Read as 1 if the timestamp register is unlocked. + Read as 1 if the timestamp register is unlocked CLK_LOCK : int Bit 7 (0x80): If set to 1, the device will lock the current timestamp register counter (R_TIMESTAMP_SECOND) and reject new timestamp values. - Read as 1 if the timestamp register is locked. + Read as 1 if the timestamp register is locked """ CLK_REP = 0x01 diff --git a/harp/protocol/exceptions.py b/harp/protocol/exceptions.py index 058e23b..26b74cf 100644 --- a/harp/protocol/exceptions.py +++ b/harp/protocol/exceptions.py @@ -5,6 +5,10 @@ class HarpException(Exception): class HarpWriteException(HarpException): + """ + Exception raised when there is an error writing to a register in the Harp device. + """ + def __init__(self, register, message): super().__init__(f"Error writing to register {register}: {message}") self.register = register @@ -12,6 +16,10 @@ def __init__(self, register, message): class HarpReadException(HarpException): + """ + Exception raised when there is an error reading from a register in the Harp device. + """ + def __init__(self, register, message): super().__init__(f"Error reading from register {register}: {message}") self.register = register diff --git a/harp/protocol/messages.py b/harp/protocol/messages.py index 4437e82..a676195 100644 --- a/harp/protocol/messages.py +++ b/harp/protocol/messages.py @@ -13,19 +13,19 @@ class HarpMessage: Attributes ---------- frame : bytearray - the bytearray containing the whole Harp message + The bytearray containing the whole Harp message message_type : MessageType - the message type + The message type length : int - the length parameter of the Harp message + The length parameter of the Harp message address : int - the address of the register to which the Harp message refers to + The address of the register to which the Harp message refers to port : int - indicates the origin or destination of the Harp message in case the device is a hub of Harp devices. The value 255 points to the device itself (default value). + Indicates the origin or destination of the Harp message in case the device is a hub of Harp devices. The value 255 points to the device itself (default value). payload_type : PayloadType - the payload type + The payload type checksum : int - the sum of all bytes contained in the Harp message + The sum of all bytes contained in the Harp message """ DEFAULT_PORT: int = 255 @@ -40,7 +40,7 @@ def calculate_checksum(self) -> int: Returns ------- int - the value of the checksum + The value of the checksum """ checksum: int = 0 for i in self.frame: @@ -55,7 +55,7 @@ def frame(self) -> bytearray: Returns ------- bytearray - the bytearray containing the whole Harp message + The bytearray containing the whole Harp message """ return self._frame @@ -67,7 +67,7 @@ def message_type(self) -> MessageType: Returns ------- MessageType - the message type + The message type """ return MessageType(self._frame[0]) @@ -79,7 +79,7 @@ def length(self) -> int: Returns ------- int - the length parameter of the Harp message + The length parameter of the Harp message """ return self._frame[1] @@ -91,7 +91,7 @@ def address(self) -> int: Returns ------- int - the address of the register to which the Harp message refers to + The address of the register to which the Harp message refers to """ return self._frame[2] @@ -103,7 +103,7 @@ def port(self) -> int: Returns ------- int - the port value + The port value """ return self._frame[3] @@ -115,7 +115,7 @@ def port(self, value: int) -> None: Parameters ---------- value : int - the port value to set + The port value to set """ self._port = value @@ -127,7 +127,7 @@ def payload_type(self) -> PayloadType: Returns ------- PayloadType - the payload type + The payload type """ return PayloadType(self._frame[4]) @@ -139,7 +139,7 @@ def payload(self) -> Union[int, list[int]]: Returns ------- Union[int, list[int]] - the payload sent in the write Harp message + The payload sent in the write Harp message """ payload_start = self.BASE_LENGTH if self.payload_type & PayloadType.Timestamp: @@ -298,7 +298,7 @@ def checksum(self) -> int: Returns ------- int - the sum of all bytes contained in the Harp message + The sum of all bytes contained in the Harp message """ return self._frame[-1] @@ -310,12 +310,12 @@ def parse(frame: bytearray) -> ReplyHarpMessage: Parameters ---------- frame : bytearray - the bytearray will be parsed into a (reply) Harp message + The bytearray will be parsed into a (reply) Harp message Returns ------- ReplyHarpMessage - the Harp message object parsed from the original bytearray + The Harp message object parsed from the original bytearray """ return ReplyHarpMessage(frame) @@ -332,13 +332,13 @@ def create( Parameters ---------- message_type : MessageType - the message type. It can only be of type READ or WRITE + The message type. It can only be of type READ or WRITE address : int - the address of the register that the message will interact with + The address of the register that the message will interact with payload_type : PayloadType - the payload type + The payload type value: int | list[int] | float | list[float], optional - the payload of the message. If message_type == MessageType.WRITE, the value cannot be None + The payload of the message. If message_type == MessageType.WRITE, the value cannot be None """ if message_type == MessageType.READ: return ReadHarpMessage(payload_type, address) @@ -360,7 +360,7 @@ def __repr__(self) -> str: Returns ------- str - the debug representation of the reply message + The debug representation of the reply message """ return self.__str__() + f"\r\nRaw Frame: {self.frame}" @@ -371,7 +371,7 @@ def __str__(self) -> str: Returns ------- str - the representation of the Harp message + The representation of the Harp message """ payload_str = "" format_str = "" @@ -415,9 +415,9 @@ class ReplyHarpMessage(HarpMessage): Attributes ---------- payload : Union[int, list[int]] - the message payload formatted as the appropriate type + The message payload formatted as the appropriate type timestamp : float - the Harp timestamp at which the message was sent + The Harp timestamp at which the message was sent """ def __init__( @@ -428,7 +428,7 @@ def __init__( Parameters ---------- frame : bytearray - the Harp message in bytearray format + The Harp message in bytearray format """ self._frame = frame @@ -465,7 +465,7 @@ def timestamp(self) -> float: Returns ------- float - the Harp timestamp at which the message was sent + The Harp timestamp at which the message was sent """ return self._timestamp @@ -476,7 +476,7 @@ def payload_as_string(self) -> str: Returns ------- str - the payload parsed as a str + The payload parsed as a str """ return self._raw_payload.decode("utf-8").rstrip("\x00") @@ -508,7 +508,7 @@ class WriteHarpMessage(HarpMessage): Attributes ---------- payload : Union[int, list[int]] - the payload sent in the write Harp message + The payload sent in the write Harp message """ MESSAGE_TYPE: int = MessageType.WRITE From d760086b4b513eb01ea59e764deb09f91eb2d5df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 2 Jul 2025 10:17:02 +0100 Subject: [PATCH 130/267] Add CurrentDriver to known device names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- harp/protocol/device_names.py | 1 + 1 file changed, 1 insertion(+) diff --git a/harp/protocol/device_names.py b/harp/protocol/device_names.py index 4b7efcb..3f31656 100644 --- a/harp/protocol/device_names.py +++ b/harp/protocol/device_names.py @@ -33,6 +33,7 @@ 1236: "AnalogInput", 1248: "RgbArray", 1280: "SoundCard", + 1282: "CurrentDriver", 1296: "SyringePump", 1400: "LicketySplit", 1401: "SniffDetector", From aca830a381a52b0dd2abf44e5998811fdb9d951d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 2 Jul 2025 10:17:21 +0100 Subject: [PATCH 131/267] Reorder docs' TOC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- mkdocs.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 97b0140..56f2896 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -76,16 +76,16 @@ theme: nav: - Home: index.md + - API: + - Protocol: api/protocol.md + - Device: api/device.md + - Messages: api/messages.md - Examples: - examples/index.md - Getting Device Info: examples/get_info/get_info.md - Wait for Events: examples/wait_for_events/wait_for_events.md - Read and Write from Registers: examples/read_and_write_from_registers/read_and_write_from_registers.md - Olfactometer Example: examples/olfactometer_example/olfactometer_example.md - - API: - - Protocol: api/protocol.md - - Device: api/device.md - - Messages: api/messages.md - Devices: '*include ../harp.devices/*/mkdocs.yml' extra: From 2113d59e46bb562234d834e42bb314fa724ebfe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 2 Jul 2025 10:23:22 +0100 Subject: [PATCH 132/267] Remove type annotation on Enum MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- harp/protocol/base.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/harp/protocol/base.py b/harp/protocol/base.py index a634f52..2217dcf 100644 --- a/harp/protocol/base.py +++ b/harp/protocol/base.py @@ -25,11 +25,11 @@ class MessageType(IntEnum): The value that corresponds to a Write Error Harp message (10). Messages of this type are only meant to be send by the device """ - READ: int = 1 - WRITE: int = 2 - EVENT: int = 3 - READ_ERROR: int = 9 - WRITE_ERROR: int = 10 + READ = 1 + WRITE = 2 + EVENT = 3 + READ_ERROR = 9 + WRITE_ERROR = 10 class PayloadType(IntEnum): From ece40f72b84a25a45cb7acc1e87f1afa1e91a0d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 2 Jul 2025 10:28:53 +0100 Subject: [PATCH 133/267] Update version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3fdaa18..5700762 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "harp-protocol" -version = "0.2.0a2" +version = "0.2.0" description = "Library for data acquisition and control of devices implementing the Harp protocol." authors = [{ name= "Hardware and Software Platform, Champalimaud Foundation", email="software@research.fchampalimaud.org"}] license = "MIT" From 3515020355193925a9569fa389c7e399c94370d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Thu, 3 Jul 2025 15:41:50 +0100 Subject: [PATCH 134/267] Add github action to build and deploy docs --- .github/workflows/build-docs.yml | 56 ++++++++++++++++++++++++++++++++ mkdocs.yml | 2 +- run_docs.sh | 19 +++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/build-docs.yml create mode 100755 run_docs.sh diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml new file mode 100644 index 0000000..72c7f7e --- /dev/null +++ b/.github/workflows/build-docs.yml @@ -0,0 +1,56 @@ +name: Build Documentation + +on: + workflow_dispatch: + +jobs: + build-docs: + runs-on: ubuntu-latest + steps: + - name: Checkout pyharp repository + uses: actions/checkout@v4 + + - name: Clone harp.devices repository + uses: actions/checkout@v4 + with: + repository: fchampalimaud/harp.devices + token: ${{ secrets.HARP_DEVICES_TOKEN }} + path: harp.devices + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Install project dependencies + run: | + uv sync + + - name: Build documentation + run: | + chmod +x ./run_docs.sh + ./run_docs.sh build + + - name: Upload Build Artifact + uses: actions/upload-pages-artifact@v3 + with: + path: site + + + deploy: + name: Deploy to Github pages + needs: build-docs + + # Grant GITHUB_TOKEN the permissions required to make a Pages deployment + permissions: + pages: write # to deploy to Pages + id-token: write # to verify the deployment originates from an appropriate source + + # Deploy to the github-pages environment + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + runs-on: ubuntu-latest + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/mkdocs.yml b/mkdocs.yml index 56f2896..1cd5215 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -86,7 +86,7 @@ nav: - Wait for Events: examples/wait_for_events/wait_for_events.md - Read and Write from Registers: examples/read_and_write_from_registers/read_and_write_from_registers.md - Olfactometer Example: examples/olfactometer_example/olfactometer_example.md - - Devices: '*include ../harp.devices/*/mkdocs.yml' + - Devices: '*include ./harp.devices/*/mkdocs.yml' extra: social: diff --git a/run_docs.sh b/run_docs.sh new file mode 100755 index 0000000..d95ab3c --- /dev/null +++ b/run_docs.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# Find all subdirectories of harp.devices and join them with ':' +HARP_DEVICES_PATHS=$(find -L ./harp.devices -mindepth 1 -maxdepth 1 -type d | paste -sd ":" -) + +# Prepend to PYTHONPATH (preserve existing PYTHONPATH) +export PYTHONPATH="$HARP_DEVICES_PATHS:$PYTHONPATH" + +# Optionally print for debugging +echo "PYTHONPATH set to: $PYTHONPATH" + +# Launch mkdocs build or serve +if [ "$1" = "build" ]; then + uv run mkdocs build +elif [ "$1" = "deploy" ]; then + uv run mkdocs gh-deploy +else + uv run mkdocs serve +fi From 47cfb1d5cd32639237c753d0873a8185bb44d753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Fri, 18 Jul 2025 10:00:25 +0100 Subject: [PATCH 135/267] Update LICENSE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- LICENSE | 1 + 1 file changed, 1 insertion(+) diff --git a/LICENSE b/LICENSE index 0c33b6f..b7afd7f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,6 @@ MIT License +Copyright (c) 2020 OEPS & Filipe Carvalho Copyright (c) 2025 Hardware and Software Platform, Champalimaud Foundation Permission is hereby granted, free of charge, to any person obtaining a copy From 39e3ca354697fa35000651b259e7502129fd64c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Tue, 5 Aug 2025 09:27:13 +0100 Subject: [PATCH 136/267] Rename communication namespace to 'serial' --- harp/{communication => serial}/__init__.py | 0 harp/{communication => serial}/device.py | 5 ++--- harp/{communication => serial}/harp_serial.py | 0 3 files changed, 2 insertions(+), 3 deletions(-) rename harp/{communication => serial}/__init__.py (100%) rename harp/{communication => serial}/device.py (99%) rename harp/{communication => serial}/harp_serial.py (100%) diff --git a/harp/communication/__init__.py b/harp/serial/__init__.py similarity index 100% rename from harp/communication/__init__.py rename to harp/serial/__init__.py diff --git a/harp/communication/device.py b/harp/serial/device.py similarity index 99% rename from harp/communication/device.py rename to harp/serial/device.py index 6662c62..b9e57f6 100644 --- a/harp/communication/device.py +++ b/harp/serial/device.py @@ -7,8 +7,6 @@ from typing import Optional, Union import serial - -from harp.communication.harp_serial import HarpSerial from harp.protocol import ( ClockConfig, CommonRegisters, @@ -20,6 +18,7 @@ ) from harp.protocol.device_names import device_names from harp.protocol.messages import HarpMessage, ReplyHarpMessage +from harp.serial.harp_serial import HarpSerial class Device: @@ -67,7 +66,7 @@ class Device: TIMESTAMP_OFFSET: int _ser: HarpSerial - _dump_file_path: Path + _dump_file_path: Optional[Path] _dump_file: Optional[BufferedWriter] = None _read_timeout_s: float diff --git a/harp/communication/harp_serial.py b/harp/serial/harp_serial.py similarity index 100% rename from harp/communication/harp_serial.py rename to harp/serial/harp_serial.py From 4d9a3a410df70dd22243b4dcd705c5b463b8a2d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Tue, 5 Aug 2025 09:27:55 +0100 Subject: [PATCH 137/267] Remove unnecessary devices folder --- harp/devices/.gitignore | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 harp/devices/.gitignore diff --git a/harp/devices/.gitignore b/harp/devices/.gitignore deleted file mode 100644 index e69de29..0000000 From efa5e046e5c6e0935b1f92fbb287f61a8a10d305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 27 Aug 2025 10:02:20 +0100 Subject: [PATCH 138/267] Fix issue with Visual_en flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- harp/serial/device.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/harp/serial/device.py b/harp/serial/device.py index b9e57f6..30ef63d 100644 --- a/harp/serial/device.py +++ b/harp/serial/device.py @@ -301,7 +301,7 @@ def op_led_en(self, enable: bool) -> bool: return reply - def status_led(self, enable: bool) -> bool: + def visual_en(self, enable: bool) -> bool: """ Sets the status led of the device. @@ -323,9 +323,9 @@ def status_led(self, enable: bool) -> bool: ).payload if enable: - reg_value |= OperationCtrl.STATUS_LED + reg_value |= OperationCtrl.VISUALEN else: - reg_value &= ~OperationCtrl.STATUS_LED + reg_value &= ~OperationCtrl.VISUALEN reply = self.send( HarpMessage.create(MessageType.WRITE, address, PayloadType.U8, reg_value) From d27c2267d03bfa36b4f8422c3c8b491d59057e73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 27 Aug 2025 10:05:21 +0100 Subject: [PATCH 139/267] Fix documentation path for Device MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- docs/api/device.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/device.md b/docs/api/device.md index 451d0eb..a128d21 100644 --- a/docs/api/device.md +++ b/docs/api/device.md @@ -1 +1 @@ -::: harp.communication.Device +::: harp.serial.Device From 77607aead28e8aecf61adced90b7d54ae1e3fd3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 27 Aug 2025 10:08:23 +0100 Subject: [PATCH 140/267] Implement a Timeout handling strategy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- harp/protocol/exceptions.py | 8 +++++ harp/serial/device.py | 68 ++++++++++++++++++++++++++++++------- 2 files changed, 64 insertions(+), 12 deletions(-) diff --git a/harp/protocol/exceptions.py b/harp/protocol/exceptions.py index 26b74cf..915b9ce 100644 --- a/harp/protocol/exceptions.py +++ b/harp/protocol/exceptions.py @@ -24,3 +24,11 @@ def __init__(self, register, message): super().__init__(f"Error reading from register {register}: {message}") self.register = register self.message = message + + +class HarpTimeoutError(HarpException): + """Raised when no reply is received within the configured timeout.""" + + def __init__(self, timeout_s: float): + super().__init__(f"No reply received within {timeout_s} seconds.") + self.timeout_s = timeout_s diff --git a/harp/serial/device.py b/harp/serial/device.py index 30ef63d..8d2e020 100644 --- a/harp/serial/device.py +++ b/harp/serial/device.py @@ -2,9 +2,10 @@ import logging import queue +from enum import Enum from io import BufferedWriter from pathlib import Path -from typing import Optional, Union +from typing import Optional import serial from harp.protocol import ( @@ -17,10 +18,18 @@ ResetMode, ) from harp.protocol.device_names import device_names +from harp.protocol.exceptions import HarpTimeoutError from harp.protocol.messages import HarpMessage, ReplyHarpMessage from harp.serial.harp_serial import HarpSerial +class TimeoutStrategy(Enum): + RAISE = "raise" # Raise HarpTimeoutError + RETURN_NONE = "return_none" # Return None + LOG_AND_RAISE = "log_and_raise" + LOG_AND_NONE = "log_and_none" + + class Device: """ The `Device` class provides the interface for interacting with Harp devices. This implementation of the Harp device was based on the official documentation available on the [harp-tech website](https://harp-tech.org/protocol/Device.html). @@ -77,6 +86,7 @@ def __init__( serial_port: str, dump_file_path: Optional[str] = None, read_timeout_s: float = 1, + timeout_strategy: TimeoutStrategy = TimeoutStrategy.RAISE, ): """ Parameters @@ -94,6 +104,7 @@ def __init__( if dump_file_path is not None: self._dump_file_path = Path() / dump_file_path self._read_timeout_s = read_timeout_s + self._timeout_strategy = timeout_strategy # Connect to the Harp device and load the data stored in the device's common registers self.connect() @@ -427,43 +438,76 @@ def set_timestamp_offset(self, timestamp_offset: int) -> ReplyHarpMessage: return reply - def send(self, message: HarpMessage) -> Optional[ReplyHarpMessage]: + def send( + self, + message: HarpMessage, + *, + expect_reply: bool = True, + timeout_strategy: TimeoutStrategy | None = None, + ) -> ReplyHarpMessage | None: """ - Sends a Harp message. + Sends a Harp message and (optionally) waits for a reply. Parameters ---------- message : HarpMessage - The HarpMessage containing the message to be sent to the device + The HarpMessage to be sent to the device + expect_reply : bool, optional + If False, do not wait for a reply (fire-and-forget) + timeout_strategy : TimeoutStrategy | None + Override the device-level timeout strategy for this call Returns ------- - Optional[ReplyHarpMessage] - The reply to the Harp message or None if no reply is given + ReplyHarpMessage | None + Reply (or None when allowed by the timeout strategy or expect_reply=False) + + Raises + ------- + HarpTimeoutError + If no reply is received and the effective strategy requires raising """ self._ser.write(message.frame) - reply = self._read() - if reply is None: + if not expect_reply: return None - self._dump_reply(reply.frame) + strategy = timeout_strategy or self._timeout_strategy + + try: + reply = self._read() + except TimeoutError: + hte = HarpTimeoutError(self._read_timeout_s) + if strategy in ( + TimeoutStrategy.LOG_AND_RAISE, + TimeoutStrategy.LOG_AND_NONE, + ): + self.log.warning(str(hte)) + if strategy in (TimeoutStrategy.RAISE, TimeoutStrategy.LOG_AND_RAISE): + raise hte + return None + self._dump_reply(reply.frame) return reply - def _read(self) -> Union[ReplyHarpMessage, None]: + def _read(self) -> ReplyHarpMessage: """ Reads an incoming serial message in a blocking way. Returns ------- - Union[ReplyHarpMessage, None] + ReplyHarpMessage The incoming Harp message in case it exists + + Raises + ------- + TimeoutError + If no reply is received within the timeout period """ try: return self._ser.msg_q.get(block=True, timeout=self._read_timeout_s) except queue.Empty: - return None + raise TimeoutError("No reply received within the timeout period.") def _dump_reply(self, reply: bytes): """ From 11c47df039ac659cf8396628943f8d337449372d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 27 Aug 2025 10:13:08 +0100 Subject: [PATCH 141/267] Update payload handling taking into account the timeout strategy in send method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- harp/protocol/messages.py | 8 +- harp/serial/device.py | 276 +++++++++++++++++++++++++++++++------- 2 files changed, 229 insertions(+), 55 deletions(-) diff --git a/harp/protocol/messages.py b/harp/protocol/messages.py index a676195..9b6e366 100644 --- a/harp/protocol/messages.py +++ b/harp/protocol/messages.py @@ -1,7 +1,7 @@ from __future__ import annotations # for type hints (PEP 563) import struct -from typing import List, Union +from typing import Union, Optional from harp.protocol import MessageType, PayloadType @@ -132,7 +132,7 @@ def payload_type(self) -> PayloadType: return PayloadType(self._frame[4]) @property - def payload(self) -> Union[int, list[int]]: + def payload(self) -> Union[int, list[int], bytearray, float, list[float]]: """ The payload sent in the write Harp message. @@ -324,7 +324,7 @@ def create( message_type: MessageType, address: int, payload_type: PayloadType, - value: int | list[int] | float | list[float] = None, + value: Optional[int | list[int] | float | list[float]] = None, ) -> HarpMessage: """ Creates a Harp message. @@ -531,7 +531,7 @@ def __init__( self, payload_type: PayloadType, address: int, - value: int | float | List[int] | List[float] = None, + value: Optional[int | float | list[int] | list[float]] = None, ): """ Create a WriteHarpMessage to send to a device. diff --git a/harp/serial/device.py b/harp/serial/device.py index 8d2e020..bdc554f 100644 --- a/harp/serial/device.py +++ b/harp/serial/device.py @@ -199,7 +199,13 @@ def dump_registers(self) -> list: address = CommonRegisters.OPERATION_CTRL reg_value = self.send( HarpMessage.create(MessageType.READ, address, PayloadType.U8) - ).payload + ) + + if reg_value is None: + return [] + + reg_value = reg_value.payload + # Assert DUMP bit reg_value |= OperationCtrl.DUMP self.send( @@ -216,7 +222,7 @@ def dump_registers(self) -> list: break return replies - def set_mode(self, mode: OperationMode) -> ReplyHarpMessage: + def set_mode(self, mode: OperationMode) -> ReplyHarpMessage | None: """ Sets the operation mode of the device. @@ -235,7 +241,12 @@ def set_mode(self, mode: OperationMode) -> ReplyHarpMessage: # Read register first reg_value = self.send( HarpMessage.create(MessageType.READ, address, PayloadType.U8) - ).payload + ) + + if reg_value is None: + return reg_value + + reg_value = reg_value.payload # Clear old operation mode reg_value &= ~OperationCtrl.OP_MODE @@ -267,7 +278,12 @@ def alive_en(self, enable: bool) -> bool: # Read register first reg_value = self.send( HarpMessage.create(MessageType.READ, address, PayloadType.U8) - ).payload + ) + + if reg_value is None: + return False + + reg_value = reg_value.payload if enable: reg_value |= OperationCtrl.ALIVE_EN @@ -278,7 +294,10 @@ def alive_en(self, enable: bool) -> bool: HarpMessage.create(MessageType.WRITE, address, PayloadType.U8, reg_value) ) - return reply + if reply is None: + return False + + return reply is not None def op_led_en(self, enable: bool) -> bool: """ @@ -299,7 +318,12 @@ def op_led_en(self, enable: bool) -> bool: # Read register first reg_value = self.send( HarpMessage.create(MessageType.READ, address, PayloadType.U8) - ).payload + ) + + if reg_value is None: + return False + + reg_value = reg_value.payload if enable: reg_value |= OperationCtrl.OPLEDEN @@ -310,7 +334,7 @@ def op_led_en(self, enable: bool) -> bool: HarpMessage.create(MessageType.WRITE, address, PayloadType.U8, reg_value) ) - return reply + return reply is not None def visual_en(self, enable: bool) -> bool: """ @@ -331,7 +355,12 @@ def visual_en(self, enable: bool) -> bool: # Read register first reg_value = self.send( HarpMessage.create(MessageType.READ, address, PayloadType.U8) - ).payload + ) + + if reg_value is None: + return False + + reg_value = reg_value.payload if enable: reg_value |= OperationCtrl.VISUALEN @@ -342,7 +371,7 @@ def visual_en(self, enable: bool) -> bool: HarpMessage.create(MessageType.WRITE, address, PayloadType.U8, reg_value) ) - return reply + return reply is not None def mute_reply(self, enable: bool) -> bool: """ @@ -363,7 +392,12 @@ def mute_reply(self, enable: bool) -> bool: # Read register first reg_value = self.send( HarpMessage.create(MessageType.READ, address, PayloadType.U8) - ).payload + ) + + if reg_value is None: + return False + + reg_value = reg_value.payload if enable: reg_value |= OperationCtrl.MUTE_RPL @@ -374,11 +408,11 @@ def mute_reply(self, enable: bool) -> bool: HarpMessage.create(MessageType.WRITE, address, PayloadType.U8, reg_value) ) - return reply + return reply is not None def reset_device( self, reset_mode: ResetMode = ResetMode.RST_DEF - ) -> ReplyHarpMessage: + ) -> ReplyHarpMessage | None: """ Resets the device and reboots with all the registers with the default values. Beware that the EEPROM will be erased. More information on the reset device register can be found [here](https://harp-tech.org/protocol/Device.html#r_reset_dev-u8--reset-device-and-save-non-volatile-registers). @@ -394,7 +428,7 @@ def reset_device( return reply - def set_clock_config(self, clock_config: ClockConfig) -> ReplyHarpMessage: + def set_clock_config(self, clock_config: ClockConfig) -> ReplyHarpMessage | None: """ Sets the clock configuration of the device. @@ -415,7 +449,7 @@ def set_clock_config(self, clock_config: ClockConfig) -> ReplyHarpMessage: return reply - def set_timestamp_offset(self, timestamp_offset: int) -> ReplyHarpMessage: + def set_timestamp_offset(self, timestamp_offset: int) -> ReplyHarpMessage | None: """ When the value of this register is above 0 (zero), the device's timestamp will be offset by this amount. The register is sensitive to 500 microsecond increments. This register is non-volatile. @@ -509,7 +543,7 @@ def _read(self) -> ReplyHarpMessage: except queue.Empty: raise TimeoutError("No reply received within the timeout period.") - def _dump_reply(self, reply: bytes): + def _dump_reply(self, reply: bytearray): """ Dumps the reply to a Harp message in the dump file in case it exists. """ @@ -544,7 +578,7 @@ def event_count(self) -> int: """ return self._ser.event_q.qsize() - def read_u8(self, address: int) -> ReplyHarpMessage: + def read_u8(self, address: int) -> ReplyHarpMessage | None: """ Reads the value of a register of type U8. @@ -557,8 +591,13 @@ def read_u8(self, address: int) -> ReplyHarpMessage: ------- ReplyHarpMessage The reply to the Harp message that will contain the value read from the register + + Raises + ------ + HarpTimeoutError + If no reply is received and the effective strategy requires raising """ - return self.send( + reply = self.send( HarpMessage.create( message_type=MessageType.READ, address=address, @@ -566,7 +605,9 @@ def read_u8(self, address: int) -> ReplyHarpMessage: ) ) - def read_s8(self, address: int) -> ReplyHarpMessage: + return reply + + def read_s8(self, address: int) -> ReplyHarpMessage | None: """ Reads the value of a register of type S8. @@ -579,8 +620,13 @@ def read_s8(self, address: int) -> ReplyHarpMessage: ------- ReplyHarpMessage The reply to the Harp message that will contain the value read from the register + + Raises + ------ + HarpTimeoutError + If no reply is received and the effective strategy requires raising """ - return self.send( + reply = self.send( HarpMessage.create( message_type=MessageType.READ, address=address, @@ -588,7 +634,9 @@ def read_s8(self, address: int) -> ReplyHarpMessage: ) ) - def read_u16(self, address: int) -> ReplyHarpMessage: + return reply + + def read_u16(self, address: int) -> ReplyHarpMessage | None: """ Reads the value of a register of type U16. @@ -601,8 +649,13 @@ def read_u16(self, address: int) -> ReplyHarpMessage: ------- ReplyHarpMessage The reply to the Harp message that will contain the value read from the register + + Raises + ------ + HarpTimeoutError + If no reply is received and the effective strategy requires raising """ - return self.send( + reply = self.send( HarpMessage.create( message_type=MessageType.READ, address=address, @@ -610,7 +663,9 @@ def read_u16(self, address: int) -> ReplyHarpMessage: ) ) - def read_s16(self, address: int) -> ReplyHarpMessage: + return reply + + def read_s16(self, address: int) -> ReplyHarpMessage | None: """ Reads the value of a register of type S16. @@ -623,8 +678,13 @@ def read_s16(self, address: int) -> ReplyHarpMessage: ------- ReplyHarpMessage The reply to the Harp message that will contain the value read from the register + + Raises + ------ + HarpTimeoutError + If no reply is received and the effective strategy requires raising """ - return self.send( + reply = self.send( HarpMessage.create( message_type=MessageType.READ, address=address, @@ -632,7 +692,9 @@ def read_s16(self, address: int) -> ReplyHarpMessage: ) ) - def read_u32(self, address: int) -> ReplyHarpMessage: + return reply + + def read_u32(self, address: int) -> ReplyHarpMessage | None: """ Reads the value of a register of type U32. @@ -645,8 +707,13 @@ def read_u32(self, address: int) -> ReplyHarpMessage: ------- ReplyHarpMessage The reply to the Harp message that will contain the value read from the register + + Raises + ------ + HarpTimeoutError + If no reply is received and the effective strategy requires raising """ - return self.send( + reply = self.send( HarpMessage.create( message_type=MessageType.READ, address=address, @@ -654,7 +721,9 @@ def read_u32(self, address: int) -> ReplyHarpMessage: ) ) - def read_s32(self, address: int) -> ReplyHarpMessage: + return reply + + def read_s32(self, address: int) -> ReplyHarpMessage | None: """ Reads the value of a register of type S32. @@ -667,8 +736,13 @@ def read_s32(self, address: int) -> ReplyHarpMessage: ------- ReplyHarpMessage The reply to the Harp message that will contain the value read from the register + + Raises + ------ + HarpTimeoutError + If no reply is received and the effective strategy requires raising """ - return self.send( + reply = self.send( HarpMessage.create( message_type=MessageType.READ, address=address, @@ -676,7 +750,9 @@ def read_s32(self, address: int) -> ReplyHarpMessage: ) ) - def read_u64(self, address: int) -> ReplyHarpMessage: + return reply + + def read_u64(self, address: int) -> ReplyHarpMessage | None: """ Reads the value of a register of type U64. @@ -689,8 +765,13 @@ def read_u64(self, address: int) -> ReplyHarpMessage: ------- ReplyHarpMessage The reply to the Harp message that will contain the value read from the register + + Raises + ------ + HarpTimeoutError + If no reply is received and the effective strategy requires raising """ - return self.send( + reply = self.send( HarpMessage.create( message_type=MessageType.READ, address=address, @@ -698,7 +779,9 @@ def read_u64(self, address: int) -> ReplyHarpMessage: ) ) - def read_s64(self, address: int) -> ReplyHarpMessage: + return reply + + def read_s64(self, address: int) -> ReplyHarpMessage | None: """ Reads the value of a register of type S64. @@ -711,8 +794,13 @@ def read_s64(self, address: int) -> ReplyHarpMessage: ------- ReplyHarpMessage The reply to the Harp message that will contain the value read from the register + + Raises + ------ + HarpTimeoutError + If no reply is received and the effective strategy requires raising """ - return self.send( + reply = self.send( HarpMessage.create( message_type=MessageType.READ, address=address, @@ -720,7 +808,9 @@ def read_s64(self, address: int) -> ReplyHarpMessage: ) ) - def read_float(self, address: int) -> ReplyHarpMessage: + return reply + + def read_float(self, address: int) -> ReplyHarpMessage | None: """ Reads the value of a register of type Float. @@ -733,8 +823,13 @@ def read_float(self, address: int) -> ReplyHarpMessage: ------- ReplyHarpMessage The reply to the Harp message that will contain the value read from the register + + Raises + ------ + HarpTimeoutError + If no reply is received and the effective strategy requires raising """ - return self.send( + reply = self.send( HarpMessage.create( message_type=MessageType.READ, address=address, @@ -742,7 +837,9 @@ def read_float(self, address: int) -> ReplyHarpMessage: ) ) - def write_u8(self, address: int, value: int | list[int]) -> ReplyHarpMessage: + return reply + + def write_u8(self, address: int, value: int | list[int]) -> ReplyHarpMessage | None: """ Writes the value of a register of type U8. @@ -757,8 +854,13 @@ def write_u8(self, address: int, value: int | list[int]) -> ReplyHarpMessage: ------- ReplyHarpMessage The reply to the Harp message + + Raises + ------ + HarpTimeoutError + If no reply is received and the effective strategy requires raising """ - return self.send( + reply = self.send( HarpMessage.create( message_type=MessageType.WRITE, address=address, @@ -767,7 +869,9 @@ def write_u8(self, address: int, value: int | list[int]) -> ReplyHarpMessage: ) ) - def write_s8(self, address: int, value: int | list[int]) -> ReplyHarpMessage: + return reply + + def write_s8(self, address: int, value: int | list[int]) -> ReplyHarpMessage | None: """ Writes the value of a register of type S8. @@ -782,8 +886,13 @@ def write_s8(self, address: int, value: int | list[int]) -> ReplyHarpMessage: ------- ReplyHarpMessage The reply to the Harp message + + Raises + ------ + HarpTimeoutError + If no reply is received and the effective strategy requires raising """ - return self.send( + reply = self.send( HarpMessage.create( message_type=MessageType.WRITE, address=address, @@ -792,7 +901,11 @@ def write_s8(self, address: int, value: int | list[int]) -> ReplyHarpMessage: ) ) - def write_u16(self, address: int, value: int | list[int]) -> ReplyHarpMessage: + return reply + + def write_u16( + self, address: int, value: int | list[int] + ) -> ReplyHarpMessage | None: """ Writes the value of a register of type U16. @@ -807,8 +920,13 @@ def write_u16(self, address: int, value: int | list[int]) -> ReplyHarpMessage: ------- ReplyHarpMessage The reply to the Harp message + + Raises + ------ + HarpTimeoutError + If no reply is received and the effective strategy requires raising """ - return self.send( + reply = self.send( HarpMessage.create( message_type=MessageType.WRITE, address=address, @@ -817,7 +935,11 @@ def write_u16(self, address: int, value: int | list[int]) -> ReplyHarpMessage: ) ) - def write_s16(self, address: int, value: int | list[int]) -> ReplyHarpMessage: + return reply + + def write_s16( + self, address: int, value: int | list[int] + ) -> ReplyHarpMessage | None: """ Writes the value of a register of type S16. @@ -832,8 +954,13 @@ def write_s16(self, address: int, value: int | list[int]) -> ReplyHarpMessage: ------- ReplyHarpMessage The reply to the Harp message + + Raises + ------ + HarpTimeoutError + If no reply is received and the effective strategy requires raising """ - return self.send( + reply = self.send( HarpMessage.create( message_type=MessageType.WRITE, address=address, @@ -842,7 +969,11 @@ def write_s16(self, address: int, value: int | list[int]) -> ReplyHarpMessage: ) ) - def write_u32(self, address: int, value: int | list[int]) -> ReplyHarpMessage: + return reply + + def write_u32( + self, address: int, value: int | list[int] + ) -> ReplyHarpMessage | None: """ Writes the value of a register of type U32. @@ -857,8 +988,13 @@ def write_u32(self, address: int, value: int | list[int]) -> ReplyHarpMessage: ------- ReplyHarpMessage The reply to the Harp message + + Raises + ------ + HarpTimeoutError + If no reply is received and the effective strategy requires raising """ - return self.send( + reply = self.send( HarpMessage.create( message_type=MessageType.WRITE, address=address, @@ -867,7 +1003,11 @@ def write_u32(self, address: int, value: int | list[int]) -> ReplyHarpMessage: ) ) - def write_s32(self, address: int, value: int | list[int]) -> ReplyHarpMessage: + return reply + + def write_s32( + self, address: int, value: int | list[int] + ) -> ReplyHarpMessage | None: """ Writes the value of a register of type S32. @@ -882,8 +1022,13 @@ def write_s32(self, address: int, value: int | list[int]) -> ReplyHarpMessage: ------- ReplyHarpMessage The reply to the Harp message + + Raises + ------ + HarpTimeoutError + If no reply is received and the effective strategy requires raising """ - return self.send( + reply = self.send( HarpMessage.create( message_type=MessageType.WRITE, address=address, @@ -892,7 +1037,11 @@ def write_s32(self, address: int, value: int | list[int]) -> ReplyHarpMessage: ) ) - def write_u64(self, address: int, value: int | list[int]) -> ReplyHarpMessage: + return reply + + def write_u64( + self, address: int, value: int | list[int] + ) -> ReplyHarpMessage | None: """ Writes the value of a register of type U64. @@ -907,8 +1056,13 @@ def write_u64(self, address: int, value: int | list[int]) -> ReplyHarpMessage: ------- ReplyHarpMessage The reply to the Harp message + + Raises + ------ + HarpTimeoutError + If no reply is received and the effective strategy requires raising """ - return self.send( + reply = self.send( HarpMessage.create( message_type=MessageType.WRITE, address=address, @@ -917,7 +1071,11 @@ def write_u64(self, address: int, value: int | list[int]) -> ReplyHarpMessage: ) ) - def write_s64(self, address: int, value: int | list[int]) -> ReplyHarpMessage: + return reply + + def write_s64( + self, address: int, value: int | list[int] + ) -> ReplyHarpMessage | None: """ Writes the value of a register of type S64. @@ -932,8 +1090,13 @@ def write_s64(self, address: int, value: int | list[int]) -> ReplyHarpMessage: ------- ReplyHarpMessage The reply to the Harp message + + Raises + ------ + HarpTimeoutError + If no reply is received and the effective strategy requires raising """ - return self.send( + reply = self.send( HarpMessage.create( message_type=MessageType.WRITE, address=address, @@ -942,7 +1105,11 @@ def write_s64(self, address: int, value: int | list[int]) -> ReplyHarpMessage: ) ) - def write_float(self, address: int, value: float | list[float]) -> ReplyHarpMessage: + return reply + + def write_float( + self, address: int, value: float | list[float] + ) -> ReplyHarpMessage | None: """ Writes the value of a register of type Float. @@ -957,8 +1124,13 @@ def write_float(self, address: int, value: float | list[float]) -> ReplyHarpMess ------- ReplyHarpMessage The reply to the Harp message + + Raises + ------ + HarpTimeoutError + If no reply is received and the effective strategy requires raising """ - return self.send( + reply = self.send( HarpMessage.create( message_type=MessageType.WRITE, address=address, @@ -967,6 +1139,8 @@ def write_float(self, address: int, value: float | list[float]) -> ReplyHarpMess ) ) + return reply + def _read_who_am_i(self) -> int: """ Reads the value stored in the `WHO_AM_I` register. From a0be5e27d02130a9b4fb4bbcf3be72f1aa43ddc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 27 Aug 2025 15:21:44 +0100 Subject: [PATCH 142/267] Reorganize project directory structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- {harp => src/harp-protocol/harp}/protocol/__init__.py | 0 {harp => src/harp-protocol/harp}/protocol/base.py | 0 {harp => src/harp-protocol/harp}/protocol/device_names.py | 0 {harp => src/harp-protocol/harp}/protocol/exceptions.py | 0 {harp => src/harp-protocol/harp}/protocol/messages.py | 0 {harp => src/harp-serial/harp}/serial/__init__.py | 0 {harp => src/harp-serial/harp}/serial/device.py | 0 {harp => src/harp-serial/harp}/serial/harp_serial.py | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename {harp => src/harp-protocol/harp}/protocol/__init__.py (100%) rename {harp => src/harp-protocol/harp}/protocol/base.py (100%) rename {harp => src/harp-protocol/harp}/protocol/device_names.py (100%) rename {harp => src/harp-protocol/harp}/protocol/exceptions.py (100%) rename {harp => src/harp-protocol/harp}/protocol/messages.py (100%) rename {harp => src/harp-serial/harp}/serial/__init__.py (100%) rename {harp => src/harp-serial/harp}/serial/device.py (100%) rename {harp => src/harp-serial/harp}/serial/harp_serial.py (100%) diff --git a/harp/protocol/__init__.py b/src/harp-protocol/harp/protocol/__init__.py similarity index 100% rename from harp/protocol/__init__.py rename to src/harp-protocol/harp/protocol/__init__.py diff --git a/harp/protocol/base.py b/src/harp-protocol/harp/protocol/base.py similarity index 100% rename from harp/protocol/base.py rename to src/harp-protocol/harp/protocol/base.py diff --git a/harp/protocol/device_names.py b/src/harp-protocol/harp/protocol/device_names.py similarity index 100% rename from harp/protocol/device_names.py rename to src/harp-protocol/harp/protocol/device_names.py diff --git a/harp/protocol/exceptions.py b/src/harp-protocol/harp/protocol/exceptions.py similarity index 100% rename from harp/protocol/exceptions.py rename to src/harp-protocol/harp/protocol/exceptions.py diff --git a/harp/protocol/messages.py b/src/harp-protocol/harp/protocol/messages.py similarity index 100% rename from harp/protocol/messages.py rename to src/harp-protocol/harp/protocol/messages.py diff --git a/harp/serial/__init__.py b/src/harp-serial/harp/serial/__init__.py similarity index 100% rename from harp/serial/__init__.py rename to src/harp-serial/harp/serial/__init__.py diff --git a/harp/serial/device.py b/src/harp-serial/harp/serial/device.py similarity index 100% rename from harp/serial/device.py rename to src/harp-serial/harp/serial/device.py diff --git a/harp/serial/harp_serial.py b/src/harp-serial/harp/serial/harp_serial.py similarity index 100% rename from harp/serial/harp_serial.py rename to src/harp-serial/harp/serial/harp_serial.py From 87a8b14b8ac5fe1fb4bf44238ba8b613a9577e23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 27 Aug 2025 15:22:56 +0100 Subject: [PATCH 143/267] Create workspaces for harp-protocol and harp-serial MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- pyproject.toml | 25 +- src/harp-protocol/pyproject.toml | 18 + src/harp-serial/pyproject.toml | 25 ++ uv.lock | 610 ++++++++++++++++++------------- 4 files changed, 402 insertions(+), 276 deletions(-) create mode 100644 src/harp-protocol/pyproject.toml create mode 100644 src/harp-serial/pyproject.toml diff --git a/pyproject.toml b/pyproject.toml index 5700762..ae3c412 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "harp-protocol" +name = "pyharp" version = "0.2.0" description = "Library for data acquisition and control of devices implementing the Harp protocol." authors = [{ name= "Hardware and Software Platform, Champalimaud Foundation", email="software@research.fchampalimaud.org"}] @@ -7,15 +7,22 @@ license = "MIT" readme = 'README.md' keywords = ['python', 'harp'] requires-python = ">=3.9,<4.0" -dependencies = [ - "pyserial>=3.5", -] +dependencies = ["harp-protocol", "harp-serial"] [project.urls] Repository = "https://github.com/fchampalimaud/pyharp/" "Bug Tracker" = "https://github.com/fchampalimaud/pyharp/issues" Documentation = "https://fchampalimaud.github.io/pyharp/" +[tool.uv.sources] +harp-protocol = { workspace = true } +harp-serial = { workspace = true } + +[tool.uv.workspace] +members = [ + "src/*", +] + [dependency-groups] dev = [ "griffe-fieldz>=0.2.1", @@ -32,16 +39,6 @@ dev = [ "ruff>=0.11.0", ] -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.hatch.build.targets.wheel] -include = [ - "harp", - "harp/**/*", -] - [tool.ruff.lint.pydocstyle] convention = "numpy" diff --git a/src/harp-protocol/pyproject.toml b/src/harp-protocol/pyproject.toml new file mode 100644 index 0000000..7bffea0 --- /dev/null +++ b/src/harp-protocol/pyproject.toml @@ -0,0 +1,18 @@ +[project] +name = "harp-protocol" +version = "0.3.0" +description = "Library with the base types for Harp protocol usage." +authors = [{ name= "Hardware and Software Platform, Champalimaud Foundation", email="software@research.fchampalimaud.org"}] +license = "MIT" +keywords = ['python', 'harp'] +requires-python = ">=3.9,<4.0" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +include = [ + "harp", + "harp/**/*", +] diff --git a/src/harp-serial/pyproject.toml b/src/harp-serial/pyproject.toml new file mode 100644 index 0000000..0432a75 --- /dev/null +++ b/src/harp-serial/pyproject.toml @@ -0,0 +1,25 @@ +[project] +name = "harp-serial" +version = "0.3.0" +description = "Library for data acquisition and control of devices implementing the Harp protocol." +authors = [{ name= "Hardware and Software Platform, Champalimaud Foundation", email="software@research.fchampalimaud.org"}] +license = "MIT" +keywords = ['python', 'harp'] +requires-python = ">=3.9,<4.0" +dependencies = [ + "harp-protocol", + "pyserial>=3.5", +] + +[tool.uv.sources] +harp-protocol = { workspace = true } + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +include = [ + "harp", + "harp/**/*", +] diff --git a/uv.lock b/uv.lock index 343c1ec..d3ac2cc 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,17 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.9, <4.0" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version < '3.10'", +] + +[manifest] +members = [ + "harp-protocol", + "harp-serial", + "pyharp", +] [[package]] name = "babel" @@ -13,15 +24,16 @@ wheels = [ [[package]] name = "backrefs" -version = "5.8" +version = "5.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/46/caba1eb32fa5784428ab401a5487f73db4104590ecd939ed9daaf18b47e0/backrefs-5.8.tar.gz", hash = "sha256:2cab642a205ce966af3dd4b38ee36009b31fa9502a35fd61d59ccc116e40a6bd", size = 6773994, upload-time = "2025-02-25T18:15:32.003Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/a7/312f673df6a79003279e1f55619abbe7daebbb87c17c976ddc0345c04c7b/backrefs-5.9.tar.gz", hash = "sha256:808548cb708d66b82ee231f962cb36faaf4f2baab032f2fbb783e9c2fdddaa59", size = 5765857, upload-time = "2025-06-22T19:34:13.97Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/cb/d019ab87fe70e0fe3946196d50d6a4428623dc0c38a6669c8cae0320fbf3/backrefs-5.8-py310-none-any.whl", hash = "sha256:c67f6638a34a5b8730812f5101376f9d41dc38c43f1fdc35cb54700f6ed4465d", size = 380337, upload-time = "2025-02-25T16:53:14.607Z" }, - { url = "https://files.pythonhosted.org/packages/a9/86/abd17f50ee21b2248075cb6924c6e7f9d23b4925ca64ec660e869c2633f1/backrefs-5.8-py311-none-any.whl", hash = "sha256:2e1c15e4af0e12e45c8701bd5da0902d326b2e200cafcd25e49d9f06d44bb61b", size = 392142, upload-time = "2025-02-25T16:53:17.266Z" }, - { url = "https://files.pythonhosted.org/packages/b3/04/7b415bd75c8ab3268cc138c76fa648c19495fcc7d155508a0e62f3f82308/backrefs-5.8-py312-none-any.whl", hash = "sha256:bbef7169a33811080d67cdf1538c8289f76f0942ff971222a16034da88a73486", size = 398021, upload-time = "2025-02-25T16:53:26.378Z" }, - { url = "https://files.pythonhosted.org/packages/04/b8/60dcfb90eb03a06e883a92abbc2ab95c71f0d8c9dd0af76ab1d5ce0b1402/backrefs-5.8-py313-none-any.whl", hash = "sha256:e3a63b073867dbefd0536425f43db618578528e3896fb77be7141328642a1585", size = 399915, upload-time = "2025-02-25T16:53:28.167Z" }, - { url = "https://files.pythonhosted.org/packages/0c/37/fb6973edeb700f6e3d6ff222400602ab1830446c25c7b4676d8de93e65b8/backrefs-5.8-py39-none-any.whl", hash = "sha256:a66851e4533fb5b371aa0628e1fee1af05135616b86140c9d787a2ffdf4b8fdc", size = 380336, upload-time = "2025-02-25T16:53:29.858Z" }, + { url = "https://files.pythonhosted.org/packages/19/4d/798dc1f30468134906575156c089c492cf79b5a5fd373f07fe26c4d046bf/backrefs-5.9-py310-none-any.whl", hash = "sha256:db8e8ba0e9de81fcd635f440deab5ae5f2591b54ac1ebe0550a2ca063488cd9f", size = 380267, upload-time = "2025-06-22T19:34:05.252Z" }, + { url = "https://files.pythonhosted.org/packages/55/07/f0b3375bf0d06014e9787797e6b7cc02b38ac9ff9726ccfe834d94e9991e/backrefs-5.9-py311-none-any.whl", hash = "sha256:6907635edebbe9b2dc3de3a2befff44d74f30a4562adbb8b36f21252ea19c5cf", size = 392072, upload-time = "2025-06-22T19:34:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/9d/12/4f345407259dd60a0997107758ba3f221cf89a9b5a0f8ed5b961aef97253/backrefs-5.9-py312-none-any.whl", hash = "sha256:7fdf9771f63e6028d7fee7e0c497c81abda597ea45d6b8f89e8ad76994f5befa", size = 397947, upload-time = "2025-06-22T19:34:08.172Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/fa31834dc27a7f05e5290eae47c82690edc3a7b37d58f7fb35a1bdbf355b/backrefs-5.9-py313-none-any.whl", hash = "sha256:cc37b19fa219e93ff825ed1fed8879e47b4d89aa7a1884860e2db64ccd7c676b", size = 399843, upload-time = "2025-06-22T19:34:09.68Z" }, + { url = "https://files.pythonhosted.org/packages/fc/24/b29af34b2c9c41645a9f4ff117bae860291780d73880f449e0b5d948c070/backrefs-5.9-py314-none-any.whl", hash = "sha256:df5e169836cc8acb5e440ebae9aad4bf9d15e226d3bad049cf3f6a5c20cc8dc9", size = 411762, upload-time = "2025-06-22T19:34:11.037Z" }, + { url = "https://files.pythonhosted.org/packages/41/ff/392bff89415399a979be4a65357a41d92729ae8580a66073d8ec8d810f98/backrefs-5.9-py39-none-any.whl", hash = "sha256:f48ee18f6252b8f5777a22a00a09a85de0ca931658f1dd96d4406a34f3748c60", size = 380265, upload-time = "2025-06-22T19:34:12.405Z" }, ] [[package]] @@ -35,99 +47,118 @@ wheels = [ [[package]] name = "certifi" -version = "2025.1.31" +version = "2025.8.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577, upload-time = "2025-01-31T02:16:47.166Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393, upload-time = "2025-01-31T02:16:45.015Z" }, + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, ] [[package]] name = "charset-normalizer" -version = "3.4.1" +version = "3.4.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188, upload-time = "2024-12-24T18:12:35.43Z" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013, upload-time = "2024-12-24T18:09:43.671Z" }, - { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285, upload-time = "2024-12-24T18:09:48.113Z" }, - { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449, upload-time = "2024-12-24T18:09:50.845Z" }, - { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892, upload-time = "2024-12-24T18:09:52.078Z" }, - { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123, upload-time = "2024-12-24T18:09:54.575Z" }, - { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943, upload-time = "2024-12-24T18:09:57.324Z" }, - { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063, upload-time = "2024-12-24T18:09:59.794Z" }, - { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578, upload-time = "2024-12-24T18:10:02.357Z" }, - { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629, upload-time = "2024-12-24T18:10:03.678Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778, upload-time = "2024-12-24T18:10:06.197Z" }, - { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453, upload-time = "2024-12-24T18:10:08.848Z" }, - { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479, upload-time = "2024-12-24T18:10:10.044Z" }, - { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790, upload-time = "2024-12-24T18:10:11.323Z" }, - { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995, upload-time = "2024-12-24T18:10:12.838Z" }, - { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471, upload-time = "2024-12-24T18:10:14.101Z" }, - { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831, upload-time = "2024-12-24T18:10:15.512Z" }, - { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335, upload-time = "2024-12-24T18:10:18.369Z" }, - { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862, upload-time = "2024-12-24T18:10:19.743Z" }, - { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673, upload-time = "2024-12-24T18:10:21.139Z" }, - { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211, upload-time = "2024-12-24T18:10:22.382Z" }, - { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039, upload-time = "2024-12-24T18:10:24.802Z" }, - { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939, upload-time = "2024-12-24T18:10:26.124Z" }, - { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075, upload-time = "2024-12-24T18:10:30.027Z" }, - { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340, upload-time = "2024-12-24T18:10:32.679Z" }, - { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205, upload-time = "2024-12-24T18:10:34.724Z" }, - { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441, upload-time = "2024-12-24T18:10:37.574Z" }, - { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105, upload-time = "2024-12-24T18:10:38.83Z" }, - { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404, upload-time = "2024-12-24T18:10:44.272Z" }, - { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423, upload-time = "2024-12-24T18:10:45.492Z" }, - { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184, upload-time = "2024-12-24T18:10:47.898Z" }, - { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268, upload-time = "2024-12-24T18:10:50.589Z" }, - { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601, upload-time = "2024-12-24T18:10:52.541Z" }, - { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098, upload-time = "2024-12-24T18:10:53.789Z" }, - { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520, upload-time = "2024-12-24T18:10:55.048Z" }, - { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852, upload-time = "2024-12-24T18:10:57.647Z" }, - { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488, upload-time = "2024-12-24T18:10:59.43Z" }, - { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192, upload-time = "2024-12-24T18:11:00.676Z" }, - { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550, upload-time = "2024-12-24T18:11:01.952Z" }, - { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785, upload-time = "2024-12-24T18:11:03.142Z" }, - { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698, upload-time = "2024-12-24T18:11:05.834Z" }, - { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162, upload-time = "2024-12-24T18:11:07.064Z" }, - { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263, upload-time = "2024-12-24T18:11:08.374Z" }, - { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966, upload-time = "2024-12-24T18:11:09.831Z" }, - { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992, upload-time = "2024-12-24T18:11:12.03Z" }, - { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162, upload-time = "2024-12-24T18:11:13.372Z" }, - { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972, upload-time = "2024-12-24T18:11:14.628Z" }, - { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095, upload-time = "2024-12-24T18:11:17.672Z" }, - { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668, upload-time = "2024-12-24T18:11:18.989Z" }, - { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073, upload-time = "2024-12-24T18:11:21.507Z" }, - { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732, upload-time = "2024-12-24T18:11:22.774Z" }, - { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391, upload-time = "2024-12-24T18:11:24.139Z" }, - { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702, upload-time = "2024-12-24T18:11:26.535Z" }, - { url = "https://files.pythonhosted.org/packages/7f/c0/b913f8f02836ed9ab32ea643c6fe4d3325c3d8627cf6e78098671cafff86/charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", size = 197867, upload-time = "2024-12-24T18:12:10.438Z" }, - { url = "https://files.pythonhosted.org/packages/0f/6c/2bee440303d705b6fb1e2ec789543edec83d32d258299b16eed28aad48e0/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", size = 141385, upload-time = "2024-12-24T18:12:11.847Z" }, - { url = "https://files.pythonhosted.org/packages/3d/04/cb42585f07f6f9fd3219ffb6f37d5a39b4fd2db2355b23683060029c35f7/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", size = 151367, upload-time = "2024-12-24T18:12:13.177Z" }, - { url = "https://files.pythonhosted.org/packages/54/54/2412a5b093acb17f0222de007cc129ec0e0df198b5ad2ce5699355269dfe/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", size = 143928, upload-time = "2024-12-24T18:12:14.497Z" }, - { url = "https://files.pythonhosted.org/packages/5a/6d/e2773862b043dcf8a221342954f375392bb2ce6487bcd9f2c1b34e1d6781/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", size = 146203, upload-time = "2024-12-24T18:12:15.731Z" }, - { url = "https://files.pythonhosted.org/packages/b9/f8/ca440ef60d8f8916022859885f231abb07ada3c347c03d63f283bec32ef5/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", size = 148082, upload-time = "2024-12-24T18:12:18.641Z" }, - { url = "https://files.pythonhosted.org/packages/04/d2/42fd330901aaa4b805a1097856c2edf5095e260a597f65def493f4b8c833/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", size = 142053, upload-time = "2024-12-24T18:12:20.036Z" }, - { url = "https://files.pythonhosted.org/packages/9e/af/3a97a4fa3c53586f1910dadfc916e9c4f35eeada36de4108f5096cb7215f/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", size = 150625, upload-time = "2024-12-24T18:12:22.804Z" }, - { url = "https://files.pythonhosted.org/packages/26/ae/23d6041322a3556e4da139663d02fb1b3c59a23ab2e2b56432bd2ad63ded/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", size = 153549, upload-time = "2024-12-24T18:12:24.163Z" }, - { url = "https://files.pythonhosted.org/packages/94/22/b8f2081c6a77cb20d97e57e0b385b481887aa08019d2459dc2858ed64871/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", size = 150945, upload-time = "2024-12-24T18:12:25.415Z" }, - { url = "https://files.pythonhosted.org/packages/c7/0b/c5ec5092747f801b8b093cdf5610e732b809d6cb11f4c51e35fc28d1d389/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", size = 146595, upload-time = "2024-12-24T18:12:28.03Z" }, - { url = "https://files.pythonhosted.org/packages/0c/5a/0b59704c38470df6768aa154cc87b1ac7c9bb687990a1559dc8765e8627e/charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", size = 95453, upload-time = "2024-12-24T18:12:29.569Z" }, - { url = "https://files.pythonhosted.org/packages/85/2d/a9790237cb4d01a6d57afadc8573c8b73c609ade20b80f4cda30802009ee/charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", size = 102811, upload-time = "2024-12-24T18:12:30.83Z" }, - { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767, upload-time = "2024-12-24T18:12:32.852Z" }, + { url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695, upload-time = "2025-08-09T07:55:36.452Z" }, + { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153, upload-time = "2025-08-09T07:55:38.467Z" }, + { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428, upload-time = "2025-08-09T07:55:40.072Z" }, + { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627, upload-time = "2025-08-09T07:55:41.706Z" }, + { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388, upload-time = "2025-08-09T07:55:43.262Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077, upload-time = "2025-08-09T07:55:44.903Z" }, + { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631, upload-time = "2025-08-09T07:55:46.346Z" }, + { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210, upload-time = "2025-08-09T07:55:47.539Z" }, + { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739, upload-time = "2025-08-09T07:55:48.744Z" }, + { url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825, upload-time = "2025-08-09T07:55:50.305Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452, upload-time = "2025-08-09T07:55:51.461Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" }, + { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" }, + { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" }, + { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" }, + { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" }, + { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" }, + { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" }, + { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, + { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, + { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, + { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, + { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, + { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, + { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, + { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, + { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, + { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, + { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, + { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ca/9a0983dd5c8e9733565cf3db4df2b0a2e9a82659fd8aa2a868ac6e4a991f/charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05", size = 207520, upload-time = "2025-08-09T07:57:11.026Z" }, + { url = "https://files.pythonhosted.org/packages/39/c6/99271dc37243a4f925b09090493fb96c9333d7992c6187f5cfe5312008d2/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e", size = 147307, upload-time = "2025-08-09T07:57:12.4Z" }, + { url = "https://files.pythonhosted.org/packages/e4/69/132eab043356bba06eb333cc2cc60c6340857d0a2e4ca6dc2b51312886b3/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99", size = 160448, upload-time = "2025-08-09T07:57:13.712Z" }, + { url = "https://files.pythonhosted.org/packages/04/9a/914d294daa4809c57667b77470533e65def9c0be1ef8b4c1183a99170e9d/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7", size = 157758, upload-time = "2025-08-09T07:57:14.979Z" }, + { url = "https://files.pythonhosted.org/packages/b0/a8/6f5bcf1bcf63cb45625f7c5cadca026121ff8a6c8a3256d8d8cd59302663/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7", size = 152487, upload-time = "2025-08-09T07:57:16.332Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/d3d0e9592f4e504f9dea08b8db270821c909558c353dc3b457ed2509f2fb/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19", size = 150054, upload-time = "2025-08-09T07:57:17.576Z" }, + { url = "https://files.pythonhosted.org/packages/20/30/5f64fe3981677fe63fa987b80e6c01042eb5ff653ff7cec1b7bd9268e54e/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312", size = 161703, upload-time = "2025-08-09T07:57:20.012Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ef/dd08b2cac9284fd59e70f7d97382c33a3d0a926e45b15fc21b3308324ffd/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc", size = 159096, upload-time = "2025-08-09T07:57:21.329Z" }, + { url = "https://files.pythonhosted.org/packages/45/8c/dcef87cfc2b3f002a6478f38906f9040302c68aebe21468090e39cde1445/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34", size = 153852, upload-time = "2025-08-09T07:57:22.608Z" }, + { url = "https://files.pythonhosted.org/packages/63/86/9cbd533bd37883d467fcd1bd491b3547a3532d0fbb46de2b99feeebf185e/charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432", size = 99840, upload-time = "2025-08-09T07:57:23.883Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d6/7e805c8e5c46ff9729c49950acc4ee0aeb55efb8b3a56687658ad10c3216/charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca", size = 107438, upload-time = "2025-08-09T07:57:25.287Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, ] [[package]] name = "click" version = "8.1.8" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, ] +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -139,72 +170,97 @@ wheels = [ [[package]] name = "coverage" -version = "7.8.0" +version = "7.10.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872, upload-time = "2025-03-30T20:36:45.376Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/83/153f54356c7c200013a752ce1ed5448573dca546ce125801afca9e1ac1a4/coverage-7.10.5.tar.gz", hash = "sha256:f2e57716a78bc3ae80b2207be0709a3b2b63b9f2dcf9740ee6ac03588a2015b6", size = 821662, upload-time = "2025-08-23T14:42:44.78Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/01/1c5e6ee4ebaaa5e079db933a9a45f61172048c7efa06648445821a201084/coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe", size = 211379, upload-time = "2025-03-30T20:34:53.904Z" }, - { url = "https://files.pythonhosted.org/packages/e9/16/a463389f5ff916963471f7c13585e5f38c6814607306b3cb4d6b4cf13384/coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28", size = 211814, upload-time = "2025-03-30T20:34:56.959Z" }, - { url = "https://files.pythonhosted.org/packages/b8/b1/77062b0393f54d79064dfb72d2da402657d7c569cfbc724d56ac0f9c67ed/coverage-7.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3", size = 240937, upload-time = "2025-03-30T20:34:58.751Z" }, - { url = "https://files.pythonhosted.org/packages/d7/54/c7b00a23150083c124e908c352db03bcd33375494a4beb0c6d79b35448b9/coverage-7.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676", size = 238849, upload-time = "2025-03-30T20:35:00.521Z" }, - { url = "https://files.pythonhosted.org/packages/f7/ec/a6b7cfebd34e7b49f844788fda94713035372b5200c23088e3bbafb30970/coverage-7.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d", size = 239986, upload-time = "2025-03-30T20:35:02.307Z" }, - { url = "https://files.pythonhosted.org/packages/21/8c/c965ecef8af54e6d9b11bfbba85d4f6a319399f5f724798498387f3209eb/coverage-7.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a", size = 239896, upload-time = "2025-03-30T20:35:04.141Z" }, - { url = "https://files.pythonhosted.org/packages/40/83/070550273fb4c480efa8381735969cb403fa8fd1626d74865bfaf9e4d903/coverage-7.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c", size = 238613, upload-time = "2025-03-30T20:35:05.889Z" }, - { url = "https://files.pythonhosted.org/packages/07/76/fbb2540495b01d996d38e9f8897b861afed356be01160ab4e25471f4fed1/coverage-7.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f", size = 238909, upload-time = "2025-03-30T20:35:07.76Z" }, - { url = "https://files.pythonhosted.org/packages/a3/7e/76d604db640b7d4a86e5dd730b73e96e12a8185f22b5d0799025121f4dcb/coverage-7.8.0-cp310-cp310-win32.whl", hash = "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f", size = 213948, upload-time = "2025-03-30T20:35:09.144Z" }, - { url = "https://files.pythonhosted.org/packages/5c/a7/f8ce4aafb4a12ab475b56c76a71a40f427740cf496c14e943ade72e25023/coverage-7.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23", size = 214844, upload-time = "2025-03-30T20:35:10.734Z" }, - { url = "https://files.pythonhosted.org/packages/2b/77/074d201adb8383addae5784cb8e2dac60bb62bfdf28b2b10f3a3af2fda47/coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27", size = 211493, upload-time = "2025-03-30T20:35:12.286Z" }, - { url = "https://files.pythonhosted.org/packages/a9/89/7a8efe585750fe59b48d09f871f0e0c028a7b10722b2172dfe021fa2fdd4/coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea", size = 211921, upload-time = "2025-03-30T20:35:14.18Z" }, - { url = "https://files.pythonhosted.org/packages/e9/ef/96a90c31d08a3f40c49dbe897df4f1fd51fb6583821a1a1c5ee30cc8f680/coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7", size = 244556, upload-time = "2025-03-30T20:35:15.616Z" }, - { url = "https://files.pythonhosted.org/packages/89/97/dcd5c2ce72cee9d7b0ee8c89162c24972fb987a111b92d1a3d1d19100c61/coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040", size = 242245, upload-time = "2025-03-30T20:35:18.648Z" }, - { url = "https://files.pythonhosted.org/packages/b2/7b/b63cbb44096141ed435843bbb251558c8e05cc835c8da31ca6ffb26d44c0/coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543", size = 244032, upload-time = "2025-03-30T20:35:20.131Z" }, - { url = "https://files.pythonhosted.org/packages/97/e3/7fa8c2c00a1ef530c2a42fa5df25a6971391f92739d83d67a4ee6dcf7a02/coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2", size = 243679, upload-time = "2025-03-30T20:35:21.636Z" }, - { url = "https://files.pythonhosted.org/packages/4f/b3/e0a59d8df9150c8a0c0841d55d6568f0a9195692136c44f3d21f1842c8f6/coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318", size = 241852, upload-time = "2025-03-30T20:35:23.525Z" }, - { url = "https://files.pythonhosted.org/packages/9b/82/db347ccd57bcef150c173df2ade97976a8367a3be7160e303e43dd0c795f/coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9", size = 242389, upload-time = "2025-03-30T20:35:25.09Z" }, - { url = "https://files.pythonhosted.org/packages/21/f6/3f7d7879ceb03923195d9ff294456241ed05815281f5254bc16ef71d6a20/coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c", size = 213997, upload-time = "2025-03-30T20:35:26.914Z" }, - { url = "https://files.pythonhosted.org/packages/28/87/021189643e18ecf045dbe1e2071b2747901f229df302de01c998eeadf146/coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78", size = 214911, upload-time = "2025-03-30T20:35:28.498Z" }, - { url = "https://files.pythonhosted.org/packages/aa/12/4792669473297f7973518bec373a955e267deb4339286f882439b8535b39/coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc", size = 211684, upload-time = "2025-03-30T20:35:29.959Z" }, - { url = "https://files.pythonhosted.org/packages/be/e1/2a4ec273894000ebedd789e8f2fc3813fcaf486074f87fd1c5b2cb1c0a2b/coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6", size = 211935, upload-time = "2025-03-30T20:35:31.912Z" }, - { url = "https://files.pythonhosted.org/packages/f8/3a/7b14f6e4372786709a361729164125f6b7caf4024ce02e596c4a69bccb89/coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d", size = 245994, upload-time = "2025-03-30T20:35:33.455Z" }, - { url = "https://files.pythonhosted.org/packages/54/80/039cc7f1f81dcbd01ea796d36d3797e60c106077e31fd1f526b85337d6a1/coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05", size = 242885, upload-time = "2025-03-30T20:35:35.354Z" }, - { url = "https://files.pythonhosted.org/packages/10/e0/dc8355f992b6cc2f9dcd5ef6242b62a3f73264893bc09fbb08bfcab18eb4/coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a", size = 245142, upload-time = "2025-03-30T20:35:37.121Z" }, - { url = "https://files.pythonhosted.org/packages/43/1b/33e313b22cf50f652becb94c6e7dae25d8f02e52e44db37a82de9ac357e8/coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6", size = 244906, upload-time = "2025-03-30T20:35:39.07Z" }, - { url = "https://files.pythonhosted.org/packages/05/08/c0a8048e942e7f918764ccc99503e2bccffba1c42568693ce6955860365e/coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47", size = 243124, upload-time = "2025-03-30T20:35:40.598Z" }, - { url = "https://files.pythonhosted.org/packages/5b/62/ea625b30623083c2aad645c9a6288ad9fc83d570f9adb913a2abdba562dd/coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe", size = 244317, upload-time = "2025-03-30T20:35:42.204Z" }, - { url = "https://files.pythonhosted.org/packages/62/cb/3871f13ee1130a6c8f020e2f71d9ed269e1e2124aa3374d2180ee451cee9/coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545", size = 214170, upload-time = "2025-03-30T20:35:44.216Z" }, - { url = "https://files.pythonhosted.org/packages/88/26/69fe1193ab0bfa1eb7a7c0149a066123611baba029ebb448500abd8143f9/coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b", size = 214969, upload-time = "2025-03-30T20:35:45.797Z" }, - { url = "https://files.pythonhosted.org/packages/f3/21/87e9b97b568e223f3438d93072479c2f36cc9b3f6b9f7094b9d50232acc0/coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd", size = 211708, upload-time = "2025-03-30T20:35:47.417Z" }, - { url = "https://files.pythonhosted.org/packages/75/be/882d08b28a0d19c9c4c2e8a1c6ebe1f79c9c839eb46d4fca3bd3b34562b9/coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00", size = 211981, upload-time = "2025-03-30T20:35:49.002Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1d/ce99612ebd58082fbe3f8c66f6d8d5694976c76a0d474503fa70633ec77f/coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64", size = 245495, upload-time = "2025-03-30T20:35:51.073Z" }, - { url = "https://files.pythonhosted.org/packages/dc/8d/6115abe97df98db6b2bd76aae395fcc941d039a7acd25f741312ced9a78f/coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067", size = 242538, upload-time = "2025-03-30T20:35:52.941Z" }, - { url = "https://files.pythonhosted.org/packages/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008", size = 244561, upload-time = "2025-03-30T20:35:54.658Z" }, - { url = "https://files.pythonhosted.org/packages/22/70/c10c77cd77970ac965734fe3419f2c98665f6e982744a9bfb0e749d298f4/coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733", size = 244633, upload-time = "2025-03-30T20:35:56.221Z" }, - { url = "https://files.pythonhosted.org/packages/38/5a/4f7569d946a07c952688debee18c2bb9ab24f88027e3d71fd25dbc2f9dca/coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323", size = 242712, upload-time = "2025-03-30T20:35:57.801Z" }, - { url = "https://files.pythonhosted.org/packages/bb/a1/03a43b33f50475a632a91ea8c127f7e35e53786dbe6781c25f19fd5a65f8/coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3", size = 244000, upload-time = "2025-03-30T20:35:59.378Z" }, - { url = "https://files.pythonhosted.org/packages/6a/89/ab6c43b1788a3128e4d1b7b54214548dcad75a621f9d277b14d16a80d8a1/coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d", size = 214195, upload-time = "2025-03-30T20:36:01.005Z" }, - { url = "https://files.pythonhosted.org/packages/12/12/6bf5f9a8b063d116bac536a7fb594fc35cb04981654cccb4bbfea5dcdfa0/coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487", size = 214998, upload-time = "2025-03-30T20:36:03.006Z" }, - { url = "https://files.pythonhosted.org/packages/2a/e6/1e9df74ef7a1c983a9c7443dac8aac37a46f1939ae3499424622e72a6f78/coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25", size = 212541, upload-time = "2025-03-30T20:36:04.638Z" }, - { url = "https://files.pythonhosted.org/packages/04/51/c32174edb7ee49744e2e81c4b1414ac9df3dacfcb5b5f273b7f285ad43f6/coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42", size = 212767, upload-time = "2025-03-30T20:36:06.503Z" }, - { url = "https://files.pythonhosted.org/packages/e9/8f/f454cbdb5212f13f29d4a7983db69169f1937e869a5142bce983ded52162/coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502", size = 256997, upload-time = "2025-03-30T20:36:08.137Z" }, - { url = "https://files.pythonhosted.org/packages/e6/74/2bf9e78b321216d6ee90a81e5c22f912fc428442c830c4077b4a071db66f/coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1", size = 252708, upload-time = "2025-03-30T20:36:09.781Z" }, - { url = "https://files.pythonhosted.org/packages/92/4d/50d7eb1e9a6062bee6e2f92e78b0998848a972e9afad349b6cdde6fa9e32/coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4", size = 255046, upload-time = "2025-03-30T20:36:11.409Z" }, - { url = "https://files.pythonhosted.org/packages/40/9e/71fb4e7402a07c4198ab44fc564d09d7d0ffca46a9fb7b0a7b929e7641bd/coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73", size = 256139, upload-time = "2025-03-30T20:36:13.86Z" }, - { url = "https://files.pythonhosted.org/packages/49/1a/78d37f7a42b5beff027e807c2843185961fdae7fe23aad5a4837c93f9d25/coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a", size = 254307, upload-time = "2025-03-30T20:36:16.074Z" }, - { url = "https://files.pythonhosted.org/packages/58/e9/8fb8e0ff6bef5e170ee19d59ca694f9001b2ec085dc99b4f65c128bb3f9a/coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883", size = 255116, upload-time = "2025-03-30T20:36:18.033Z" }, - { url = "https://files.pythonhosted.org/packages/56/b0/d968ecdbe6fe0a863de7169bbe9e8a476868959f3af24981f6a10d2b6924/coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada", size = 214909, upload-time = "2025-03-30T20:36:19.644Z" }, - { url = "https://files.pythonhosted.org/packages/87/e9/d6b7ef9fecf42dfb418d93544af47c940aa83056c49e6021a564aafbc91f/coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257", size = 216068, upload-time = "2025-03-30T20:36:21.282Z" }, - { url = "https://files.pythonhosted.org/packages/60/0c/5da94be095239814bf2730a28cffbc48d6df4304e044f80d39e1ae581997/coverage-7.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa260de59dfb143af06dcf30c2be0b200bed2a73737a8a59248fcb9fa601ef0f", size = 211377, upload-time = "2025-03-30T20:36:23.298Z" }, - { url = "https://files.pythonhosted.org/packages/d5/cb/b9e93ebf193a0bb89dbcd4f73d7b0e6ecb7c1b6c016671950e25f041835e/coverage-7.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:96121edfa4c2dfdda409877ea8608dd01de816a4dc4a0523356067b305e4e17a", size = 211803, upload-time = "2025-03-30T20:36:25.74Z" }, - { url = "https://files.pythonhosted.org/packages/78/1a/cdbfe9e1bb14d3afcaf6bb6e1b9ba76c72666e329cd06865bbd241efd652/coverage-7.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8af63b9afa1031c0ef05b217faa598f3069148eeee6bb24b79da9012423b82", size = 240561, upload-time = "2025-03-30T20:36:27.548Z" }, - { url = "https://files.pythonhosted.org/packages/59/04/57f1223f26ac018d7ce791bfa65b0c29282de3e041c1cd3ed430cfeac5a5/coverage-7.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89b1f4af0d4afe495cd4787a68e00f30f1d15939f550e869de90a86efa7e0814", size = 238488, upload-time = "2025-03-30T20:36:29.175Z" }, - { url = "https://files.pythonhosted.org/packages/b7/b1/0f25516ae2a35e265868670384feebe64e7857d9cffeeb3887b0197e2ba2/coverage-7.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94ec0be97723ae72d63d3aa41961a0b9a6f5a53ff599813c324548d18e3b9e8c", size = 239589, upload-time = "2025-03-30T20:36:30.876Z" }, - { url = "https://files.pythonhosted.org/packages/e0/a4/99d88baac0d1d5a46ceef2dd687aac08fffa8795e4c3e71b6f6c78e14482/coverage-7.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8a1d96e780bdb2d0cbb297325711701f7c0b6f89199a57f2049e90064c29f6bd", size = 239366, upload-time = "2025-03-30T20:36:32.563Z" }, - { url = "https://files.pythonhosted.org/packages/ea/9e/1db89e135feb827a868ed15f8fc857160757f9cab140ffee21342c783ceb/coverage-7.8.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f1d8a2a57b47142b10374902777e798784abf400a004b14f1b0b9eaf1e528ba4", size = 237591, upload-time = "2025-03-30T20:36:34.721Z" }, - { url = "https://files.pythonhosted.org/packages/1b/6d/ac4d6fdfd0e201bc82d1b08adfacb1e34b40d21a22cdd62cfaf3c1828566/coverage-7.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cf60dd2696b457b710dd40bf17ad269d5f5457b96442f7f85722bdb16fa6c899", size = 238572, upload-time = "2025-03-30T20:36:36.805Z" }, - { url = "https://files.pythonhosted.org/packages/25/5e/917cbe617c230f7f1745b6a13e780a3a1cd1cf328dbcd0fd8d7ec52858cd/coverage-7.8.0-cp39-cp39-win32.whl", hash = "sha256:be945402e03de47ba1872cd5236395e0f4ad635526185a930735f66710e1bd3f", size = 213966, upload-time = "2025-03-30T20:36:38.551Z" }, - { url = "https://files.pythonhosted.org/packages/bd/93/72b434fe550135869f9ea88dd36068af19afce666db576e059e75177e813/coverage-7.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:90e7fbc6216ecaffa5a880cdc9c77b7418c1dcb166166b78dbc630d07f278cc3", size = 214852, upload-time = "2025-03-30T20:36:40.209Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f1/1da77bb4c920aa30e82fa9b6ea065da3467977c2e5e032e38e66f1c57ffd/coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd", size = 203443, upload-time = "2025-03-30T20:36:41.959Z" }, - { url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435, upload-time = "2025-03-30T20:36:43.61Z" }, + { url = "https://files.pythonhosted.org/packages/af/70/e77b0061a6c7157bfce645c6b9a715a08d4c86b3360a7b3252818080b817/coverage-7.10.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c6a5c3414bfc7451b879141ce772c546985163cf553f08e0f135f0699a911801", size = 216774, upload-time = "2025-08-23T14:40:26.301Z" }, + { url = "https://files.pythonhosted.org/packages/91/08/2a79de5ecf37ee40f2d898012306f11c161548753391cec763f92647837b/coverage-7.10.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bc8e4d99ce82f1710cc3c125adc30fd1487d3cf6c2cd4994d78d68a47b16989a", size = 217175, upload-time = "2025-08-23T14:40:29.142Z" }, + { url = "https://files.pythonhosted.org/packages/64/57/0171d69a699690149a6ba6a4eb702814448c8d617cf62dbafa7ce6bfdf63/coverage-7.10.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:02252dc1216e512a9311f596b3169fad54abcb13827a8d76d5630c798a50a754", size = 243931, upload-time = "2025-08-23T14:40:30.735Z" }, + { url = "https://files.pythonhosted.org/packages/15/06/3a67662c55656702bd398a727a7f35df598eb11104fcb34f1ecbb070291a/coverage-7.10.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:73269df37883e02d460bee0cc16be90509faea1e3bd105d77360b512d5bb9c33", size = 245740, upload-time = "2025-08-23T14:40:32.302Z" }, + { url = "https://files.pythonhosted.org/packages/00/f4/f8763aabf4dc30ef0d0012522d312f0b7f9fede6246a1f27dbcc4a1e523c/coverage-7.10.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f8a81b0614642f91c9effd53eec284f965577591f51f547a1cbeb32035b4c2f", size = 247600, upload-time = "2025-08-23T14:40:33.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/31/6632219a9065e1b83f77eda116fed4c76fb64908a6a9feae41816dab8237/coverage-7.10.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6a29f8e0adb7f8c2b95fa2d4566a1d6e6722e0a637634c6563cb1ab844427dd9", size = 245640, upload-time = "2025-08-23T14:40:35.248Z" }, + { url = "https://files.pythonhosted.org/packages/6e/e2/3dba9b86037b81649b11d192bb1df11dde9a81013e434af3520222707bc8/coverage-7.10.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fcf6ab569436b4a647d4e91accba12509ad9f2554bc93d3aee23cc596e7f99c3", size = 243659, upload-time = "2025-08-23T14:40:36.815Z" }, + { url = "https://files.pythonhosted.org/packages/02/b9/57170bd9f3e333837fc24ecc88bc70fbc2eb7ccfd0876854b0c0407078c3/coverage-7.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:90dc3d6fb222b194a5de60af8d190bedeeddcbc7add317e4a3cd333ee6b7c879", size = 244537, upload-time = "2025-08-23T14:40:38.737Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1c/93ac36ef1e8b06b8d5777393a3a40cb356f9f3dab980be40a6941e443588/coverage-7.10.5-cp310-cp310-win32.whl", hash = "sha256:414a568cd545f9dc75f0686a0049393de8098414b58ea071e03395505b73d7a8", size = 219285, upload-time = "2025-08-23T14:40:40.342Z" }, + { url = "https://files.pythonhosted.org/packages/30/95/23252277e6e5fe649d6cd3ed3f35d2307e5166de4e75e66aa7f432abc46d/coverage-7.10.5-cp310-cp310-win_amd64.whl", hash = "sha256:e551f9d03347196271935fd3c0c165f0e8c049220280c1120de0084d65e9c7ff", size = 220185, upload-time = "2025-08-23T14:40:42.026Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f2/336d34d2fc1291ca7c18eeb46f64985e6cef5a1a7ef6d9c23720c6527289/coverage-7.10.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c177e6ffe2ebc7c410785307758ee21258aa8e8092b44d09a2da767834f075f2", size = 216890, upload-time = "2025-08-23T14:40:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/92448b07cc1cf2b429d0ce635f59cf0c626a5d8de21358f11e92174ff2a6/coverage-7.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:14d6071c51ad0f703d6440827eaa46386169b5fdced42631d5a5ac419616046f", size = 217287, upload-time = "2025-08-23T14:40:45.214Z" }, + { url = "https://files.pythonhosted.org/packages/96/ba/ad5b36537c5179c808d0ecdf6e4aa7630b311b3c12747ad624dcd43a9b6b/coverage-7.10.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:61f78c7c3bc272a410c5ae3fde7792b4ffb4acc03d35a7df73ca8978826bb7ab", size = 247683, upload-time = "2025-08-23T14:40:46.791Z" }, + { url = "https://files.pythonhosted.org/packages/28/e5/fe3bbc8d097029d284b5fb305b38bb3404895da48495f05bff025df62770/coverage-7.10.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f39071caa126f69d63f99b324fb08c7b1da2ec28cbb1fe7b5b1799926492f65c", size = 249614, upload-time = "2025-08-23T14:40:48.082Z" }, + { url = "https://files.pythonhosted.org/packages/69/9c/a1c89a8c8712799efccb32cd0a1ee88e452f0c13a006b65bb2271f1ac767/coverage-7.10.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343a023193f04d46edc46b2616cdbee68c94dd10208ecd3adc56fcc54ef2baa1", size = 251719, upload-time = "2025-08-23T14:40:49.349Z" }, + { url = "https://files.pythonhosted.org/packages/e9/be/5576b5625865aa95b5633315f8f4142b003a70c3d96e76f04487c3b5cc95/coverage-7.10.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:585ffe93ae5894d1ebdee69fc0b0d4b7c75d8007983692fb300ac98eed146f78", size = 249411, upload-time = "2025-08-23T14:40:50.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/0a/e39a113d4209da0dbbc9385608cdb1b0726a4d25f78672dc51c97cfea80f/coverage-7.10.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0ef4e66f006ed181df29b59921bd8fc7ed7cd6a9289295cd8b2824b49b570df", size = 247466, upload-time = "2025-08-23T14:40:52.362Z" }, + { url = "https://files.pythonhosted.org/packages/40/cb/aebb2d8c9e3533ee340bea19b71c5b76605a0268aa49808e26fe96ec0a07/coverage-7.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eb7b0bbf7cc1d0453b843eca7b5fa017874735bef9bfdfa4121373d2cc885ed6", size = 248104, upload-time = "2025-08-23T14:40:54.064Z" }, + { url = "https://files.pythonhosted.org/packages/08/e6/26570d6ccce8ff5de912cbfd268e7f475f00597cb58da9991fa919c5e539/coverage-7.10.5-cp311-cp311-win32.whl", hash = "sha256:1d043a8a06987cc0c98516e57c4d3fc2c1591364831e9deb59c9e1b4937e8caf", size = 219327, upload-time = "2025-08-23T14:40:55.424Z" }, + { url = "https://files.pythonhosted.org/packages/79/79/5f48525e366e518b36e66167e3b6e5db6fd54f63982500c6a5abb9d3dfbd/coverage-7.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:fefafcca09c3ac56372ef64a40f5fe17c5592fab906e0fdffd09543f3012ba50", size = 220213, upload-time = "2025-08-23T14:40:56.724Z" }, + { url = "https://files.pythonhosted.org/packages/40/3c/9058128b7b0bf333130c320b1eb1ae485623014a21ee196d68f7737f8610/coverage-7.10.5-cp311-cp311-win_arm64.whl", hash = "sha256:7e78b767da8b5fc5b2faa69bb001edafcd6f3995b42a331c53ef9572c55ceb82", size = 218893, upload-time = "2025-08-23T14:40:58.011Z" }, + { url = "https://files.pythonhosted.org/packages/27/8e/40d75c7128f871ea0fd829d3e7e4a14460cad7c3826e3b472e6471ad05bd/coverage-7.10.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c2d05c7e73c60a4cecc7d9b60dbfd603b4ebc0adafaef371445b47d0f805c8a9", size = 217077, upload-time = "2025-08-23T14:40:59.329Z" }, + { url = "https://files.pythonhosted.org/packages/18/a8/f333f4cf3fb5477a7f727b4d603a2eb5c3c5611c7fe01329c2e13b23b678/coverage-7.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:32ddaa3b2c509778ed5373b177eb2bf5662405493baeff52278a0b4f9415188b", size = 217310, upload-time = "2025-08-23T14:41:00.628Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2c/fbecd8381e0a07d1547922be819b4543a901402f63930313a519b937c668/coverage-7.10.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dd382410039fe062097aa0292ab6335a3f1e7af7bba2ef8d27dcda484918f20c", size = 248802, upload-time = "2025-08-23T14:41:02.012Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bc/1011da599b414fb6c9c0f34086736126f9ff71f841755786a6b87601b088/coverage-7.10.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7fa22800f3908df31cea6fb230f20ac49e343515d968cc3a42b30d5c3ebf9b5a", size = 251550, upload-time = "2025-08-23T14:41:03.438Z" }, + { url = "https://files.pythonhosted.org/packages/4c/6f/b5c03c0c721c067d21bc697accc3642f3cef9f087dac429c918c37a37437/coverage-7.10.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f366a57ac81f5e12797136552f5b7502fa053c861a009b91b80ed51f2ce651c6", size = 252684, upload-time = "2025-08-23T14:41:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/f9/50/d474bc300ebcb6a38a1047d5c465a227605d6473e49b4e0d793102312bc5/coverage-7.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f1dc8f1980a272ad4a6c84cba7981792344dad33bf5869361576b7aef42733a", size = 250602, upload-time = "2025-08-23T14:41:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/4a/2d/548c8e04249cbba3aba6bd799efdd11eee3941b70253733f5d355d689559/coverage-7.10.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2285c04ee8676f7938b02b4936d9b9b672064daab3187c20f73a55f3d70e6b4a", size = 248724, upload-time = "2025-08-23T14:41:08.429Z" }, + { url = "https://files.pythonhosted.org/packages/e2/96/a7c3c0562266ac39dcad271d0eec8fc20ab576e3e2f64130a845ad2a557b/coverage-7.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2492e4dd9daab63f5f56286f8a04c51323d237631eb98505d87e4c4ff19ec34", size = 250158, upload-time = "2025-08-23T14:41:09.749Z" }, + { url = "https://files.pythonhosted.org/packages/f3/75/74d4be58c70c42ef0b352d597b022baf12dbe2b43e7cb1525f56a0fb1d4b/coverage-7.10.5-cp312-cp312-win32.whl", hash = "sha256:38a9109c4ee8135d5df5505384fc2f20287a47ccbe0b3f04c53c9a1989c2bbaf", size = 219493, upload-time = "2025-08-23T14:41:11.095Z" }, + { url = "https://files.pythonhosted.org/packages/4f/08/364e6012d1d4d09d1e27437382967efed971d7613f94bca9add25f0c1f2b/coverage-7.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:6b87f1ad60b30bc3c43c66afa7db6b22a3109902e28c5094957626a0143a001f", size = 220302, upload-time = "2025-08-23T14:41:12.449Z" }, + { url = "https://files.pythonhosted.org/packages/db/d5/7c8a365e1f7355c58af4fe5faf3f90cc8e587590f5854808d17ccb4e7077/coverage-7.10.5-cp312-cp312-win_arm64.whl", hash = "sha256:672a6c1da5aea6c629819a0e1461e89d244f78d7b60c424ecf4f1f2556c041d8", size = 218936, upload-time = "2025-08-23T14:41:13.872Z" }, + { url = "https://files.pythonhosted.org/packages/9f/08/4166ecfb60ba011444f38a5a6107814b80c34c717bc7a23be0d22e92ca09/coverage-7.10.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ef3b83594d933020f54cf65ea1f4405d1f4e41a009c46df629dd964fcb6e907c", size = 217106, upload-time = "2025-08-23T14:41:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/25/d7/b71022408adbf040a680b8c64bf6ead3be37b553e5844f7465643979f7ca/coverage-7.10.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b96bfdf7c0ea9faebce088a3ecb2382819da4fbc05c7b80040dbc428df6af44", size = 217353, upload-time = "2025-08-23T14:41:16.656Z" }, + { url = "https://files.pythonhosted.org/packages/74/68/21e0d254dbf8972bb8dd95e3fe7038f4be037ff04ba47d6d1b12b37510ba/coverage-7.10.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:63df1fdaffa42d914d5c4d293e838937638bf75c794cf20bee12978fc8c4e3bc", size = 248350, upload-time = "2025-08-23T14:41:18.128Z" }, + { url = "https://files.pythonhosted.org/packages/90/65/28752c3a896566ec93e0219fc4f47ff71bd2b745f51554c93e8dcb659796/coverage-7.10.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8002dc6a049aac0e81ecec97abfb08c01ef0c1fbf962d0c98da3950ace89b869", size = 250955, upload-time = "2025-08-23T14:41:19.577Z" }, + { url = "https://files.pythonhosted.org/packages/a5/eb/ca6b7967f57f6fef31da8749ea20417790bb6723593c8cd98a987be20423/coverage-7.10.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63d4bb2966d6f5f705a6b0c6784c8969c468dbc4bcf9d9ded8bff1c7e092451f", size = 252230, upload-time = "2025-08-23T14:41:20.959Z" }, + { url = "https://files.pythonhosted.org/packages/bc/29/17a411b2a2a18f8b8c952aa01c00f9284a1fbc677c68a0003b772ea89104/coverage-7.10.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1f672efc0731a6846b157389b6e6d5d5e9e59d1d1a23a5c66a99fd58339914d5", size = 250387, upload-time = "2025-08-23T14:41:22.644Z" }, + { url = "https://files.pythonhosted.org/packages/c7/89/97a9e271188c2fbb3db82235c33980bcbc733da7da6065afbaa1d685a169/coverage-7.10.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3f39cef43d08049e8afc1fde4a5da8510fc6be843f8dea350ee46e2a26b2f54c", size = 248280, upload-time = "2025-08-23T14:41:24.061Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c6/0ad7d0137257553eb4706b4ad6180bec0a1b6a648b092c5bbda48d0e5b2c/coverage-7.10.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2968647e3ed5a6c019a419264386b013979ff1fb67dd11f5c9886c43d6a31fc2", size = 249894, upload-time = "2025-08-23T14:41:26.165Z" }, + { url = "https://files.pythonhosted.org/packages/84/56/fb3aba936addb4c9e5ea14f5979393f1c2466b4c89d10591fd05f2d6b2aa/coverage-7.10.5-cp313-cp313-win32.whl", hash = "sha256:0d511dda38595b2b6934c2b730a1fd57a3635c6aa2a04cb74714cdfdd53846f4", size = 219536, upload-time = "2025-08-23T14:41:27.694Z" }, + { url = "https://files.pythonhosted.org/packages/fc/54/baacb8f2f74431e3b175a9a2881feaa8feb6e2f187a0e7e3046f3c7742b2/coverage-7.10.5-cp313-cp313-win_amd64.whl", hash = "sha256:9a86281794a393513cf117177fd39c796b3f8e3759bb2764259a2abba5cce54b", size = 220330, upload-time = "2025-08-23T14:41:29.081Z" }, + { url = "https://files.pythonhosted.org/packages/64/8a/82a3788f8e31dee51d350835b23d480548ea8621f3effd7c3ba3f7e5c006/coverage-7.10.5-cp313-cp313-win_arm64.whl", hash = "sha256:cebd8e906eb98bb09c10d1feed16096700b1198d482267f8bf0474e63a7b8d84", size = 218961, upload-time = "2025-08-23T14:41:30.511Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a1/590154e6eae07beee3b111cc1f907c30da6fc8ce0a83ef756c72f3c7c748/coverage-7.10.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0520dff502da5e09d0d20781df74d8189ab334a1e40d5bafe2efaa4158e2d9e7", size = 217819, upload-time = "2025-08-23T14:41:31.962Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ff/436ffa3cfc7741f0973c5c89405307fe39b78dcf201565b934e6616fc4ad/coverage-7.10.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d9cd64aca68f503ed3f1f18c7c9174cbb797baba02ca8ab5112f9d1c0328cd4b", size = 218040, upload-time = "2025-08-23T14:41:33.472Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ca/5787fb3d7820e66273913affe8209c534ca11241eb34ee8c4fd2aaa9dd87/coverage-7.10.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0913dd1613a33b13c4f84aa6e3f4198c1a21ee28ccb4f674985c1f22109f0aae", size = 259374, upload-time = "2025-08-23T14:41:34.914Z" }, + { url = "https://files.pythonhosted.org/packages/b5/89/21af956843896adc2e64fc075eae3c1cadb97ee0a6960733e65e696f32dd/coverage-7.10.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1b7181c0feeb06ed8a02da02792f42f829a7b29990fef52eff257fef0885d760", size = 261551, upload-time = "2025-08-23T14:41:36.333Z" }, + { url = "https://files.pythonhosted.org/packages/e1/96/390a69244ab837e0ac137989277879a084c786cf036c3c4a3b9637d43a89/coverage-7.10.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36d42b7396b605f774d4372dd9c49bed71cbabce4ae1ccd074d155709dd8f235", size = 263776, upload-time = "2025-08-23T14:41:38.25Z" }, + { url = "https://files.pythonhosted.org/packages/00/32/cfd6ae1da0a521723349f3129b2455832fc27d3f8882c07e5b6fefdd0da2/coverage-7.10.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b4fdc777e05c4940b297bf47bf7eedd56a39a61dc23ba798e4b830d585486ca5", size = 261326, upload-time = "2025-08-23T14:41:40.343Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c4/bf8d459fb4ce2201e9243ce6c015936ad283a668774430a3755f467b39d1/coverage-7.10.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:42144e8e346de44a6f1dbd0a56575dd8ab8dfa7e9007da02ea5b1c30ab33a7db", size = 259090, upload-time = "2025-08-23T14:41:42.106Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5d/a234f7409896468e5539d42234016045e4015e857488b0b5b5f3f3fa5f2b/coverage-7.10.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:66c644cbd7aed8fe266d5917e2c9f65458a51cfe5eeff9c05f15b335f697066e", size = 260217, upload-time = "2025-08-23T14:41:43.591Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/87560f036099f46c2ddd235be6476dd5c1d6be6bb57569a9348d43eeecea/coverage-7.10.5-cp313-cp313t-win32.whl", hash = "sha256:2d1b73023854068c44b0c554578a4e1ef1b050ed07cf8b431549e624a29a66ee", size = 220194, upload-time = "2025-08-23T14:41:45.051Z" }, + { url = "https://files.pythonhosted.org/packages/36/a8/04a482594fdd83dc677d4a6c7e2d62135fff5a1573059806b8383fad9071/coverage-7.10.5-cp313-cp313t-win_amd64.whl", hash = "sha256:54a1532c8a642d8cc0bd5a9a51f5a9dcc440294fd06e9dda55e743c5ec1a8f14", size = 221258, upload-time = "2025-08-23T14:41:46.44Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ad/7da28594ab66fe2bc720f1bc9b131e62e9b4c6e39f044d9a48d18429cc21/coverage-7.10.5-cp313-cp313t-win_arm64.whl", hash = "sha256:74d5b63fe3f5f5d372253a4ef92492c11a4305f3550631beaa432fc9df16fcff", size = 219521, upload-time = "2025-08-23T14:41:47.882Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7f/c8b6e4e664b8a95254c35a6c8dd0bf4db201ec681c169aae2f1256e05c85/coverage-7.10.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:68c5e0bc5f44f68053369fa0d94459c84548a77660a5f2561c5e5f1e3bed7031", size = 217090, upload-time = "2025-08-23T14:41:49.327Z" }, + { url = "https://files.pythonhosted.org/packages/44/74/3ee14ede30a6e10a94a104d1d0522d5fb909a7c7cac2643d2a79891ff3b9/coverage-7.10.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cf33134ffae93865e32e1e37df043bef15a5e857d8caebc0099d225c579b0fa3", size = 217365, upload-time = "2025-08-23T14:41:50.796Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/06ac21bf87dfb7620d1f870dfa3c2cae1186ccbcdc50b8b36e27a0d52f50/coverage-7.10.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ad8fa9d5193bafcf668231294241302b5e683a0518bf1e33a9a0dfb142ec3031", size = 248413, upload-time = "2025-08-23T14:41:52.5Z" }, + { url = "https://files.pythonhosted.org/packages/21/bc/cc5bed6e985d3a14228539631573f3863be6a2587381e8bc5fdf786377a1/coverage-7.10.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:146fa1531973d38ab4b689bc764592fe6c2f913e7e80a39e7eeafd11f0ef6db2", size = 250943, upload-time = "2025-08-23T14:41:53.922Z" }, + { url = "https://files.pythonhosted.org/packages/8d/43/6a9fc323c2c75cd80b18d58db4a25dc8487f86dd9070f9592e43e3967363/coverage-7.10.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6013a37b8a4854c478d3219ee8bc2392dea51602dd0803a12d6f6182a0061762", size = 252301, upload-time = "2025-08-23T14:41:56.528Z" }, + { url = "https://files.pythonhosted.org/packages/69/7c/3e791b8845f4cd515275743e3775adb86273576596dc9f02dca37357b4f2/coverage-7.10.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:eb90fe20db9c3d930fa2ad7a308207ab5b86bf6a76f54ab6a40be4012d88fcae", size = 250302, upload-time = "2025-08-23T14:41:58.171Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bc/5099c1e1cb0c9ac6491b281babea6ebbf999d949bf4aa8cdf4f2b53505e8/coverage-7.10.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:384b34482272e960c438703cafe63316dfbea124ac62006a455c8410bf2a2262", size = 248237, upload-time = "2025-08-23T14:41:59.703Z" }, + { url = "https://files.pythonhosted.org/packages/7e/51/d346eb750a0b2f1e77f391498b753ea906fde69cc11e4b38dca28c10c88c/coverage-7.10.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:467dc74bd0a1a7de2bedf8deaf6811f43602cb532bd34d81ffd6038d6d8abe99", size = 249726, upload-time = "2025-08-23T14:42:01.343Z" }, + { url = "https://files.pythonhosted.org/packages/a3/85/eebcaa0edafe427e93286b94f56ea7e1280f2c49da0a776a6f37e04481f9/coverage-7.10.5-cp314-cp314-win32.whl", hash = "sha256:556d23d4e6393ca898b2e63a5bca91e9ac2d5fb13299ec286cd69a09a7187fde", size = 219825, upload-time = "2025-08-23T14:42:03.263Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f7/6d43e037820742603f1e855feb23463979bf40bd27d0cde1f761dcc66a3e/coverage-7.10.5-cp314-cp314-win_amd64.whl", hash = "sha256:f4446a9547681533c8fa3e3c6cf62121eeee616e6a92bd9201c6edd91beffe13", size = 220618, upload-time = "2025-08-23T14:42:05.037Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b0/ed9432e41424c51509d1da603b0393404b828906236fb87e2c8482a93468/coverage-7.10.5-cp314-cp314-win_arm64.whl", hash = "sha256:5e78bd9cf65da4c303bf663de0d73bf69f81e878bf72a94e9af67137c69b9fe9", size = 219199, upload-time = "2025-08-23T14:42:06.662Z" }, + { url = "https://files.pythonhosted.org/packages/2f/54/5a7ecfa77910f22b659c820f67c16fc1e149ed132ad7117f0364679a8fa9/coverage-7.10.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5661bf987d91ec756a47c7e5df4fbcb949f39e32f9334ccd3f43233bbb65e508", size = 217833, upload-time = "2025-08-23T14:42:08.262Z" }, + { url = "https://files.pythonhosted.org/packages/4e/0e/25672d917cc57857d40edf38f0b867fb9627115294e4f92c8fcbbc18598d/coverage-7.10.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a46473129244db42a720439a26984f8c6f834762fc4573616c1f37f13994b357", size = 218048, upload-time = "2025-08-23T14:42:10.247Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7c/0b2b4f1c6f71885d4d4b2b8608dcfc79057adb7da4143eb17d6260389e42/coverage-7.10.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1f64b8d3415d60f24b058b58d859e9512624bdfa57a2d1f8aff93c1ec45c429b", size = 259549, upload-time = "2025-08-23T14:42:11.811Z" }, + { url = "https://files.pythonhosted.org/packages/94/73/abb8dab1609abec7308d83c6aec547944070526578ee6c833d2da9a0ad42/coverage-7.10.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:44d43de99a9d90b20e0163f9770542357f58860a26e24dc1d924643bd6aa7cb4", size = 261715, upload-time = "2025-08-23T14:42:13.505Z" }, + { url = "https://files.pythonhosted.org/packages/0b/d1/abf31de21ec92731445606b8d5e6fa5144653c2788758fcf1f47adb7159a/coverage-7.10.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a931a87e5ddb6b6404e65443b742cb1c14959622777f2a4efd81fba84f5d91ba", size = 263969, upload-time = "2025-08-23T14:42:15.422Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b3/ef274927f4ebede96056173b620db649cc9cb746c61ffc467946b9d0bc67/coverage-7.10.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9559b906a100029274448f4c8b8b0a127daa4dade5661dfd821b8c188058842", size = 261408, upload-time = "2025-08-23T14:42:16.971Z" }, + { url = "https://files.pythonhosted.org/packages/20/fc/83ca2812be616d69b4cdd4e0c62a7bc526d56875e68fd0f79d47c7923584/coverage-7.10.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b08801e25e3b4526ef9ced1aa29344131a8f5213c60c03c18fe4c6170ffa2874", size = 259168, upload-time = "2025-08-23T14:42:18.512Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/e0779e5716f72d5c9962e709d09815d02b3b54724e38567308304c3fc9df/coverage-7.10.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ed9749bb8eda35f8b636fb7632f1c62f735a236a5d4edadd8bbcc5ea0542e732", size = 260317, upload-time = "2025-08-23T14:42:20.005Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fe/4247e732f2234bb5eb9984a0888a70980d681f03cbf433ba7b48f08ca5d5/coverage-7.10.5-cp314-cp314t-win32.whl", hash = "sha256:609b60d123fc2cc63ccee6d17e4676699075db72d14ac3c107cc4976d516f2df", size = 220600, upload-time = "2025-08-23T14:42:22.027Z" }, + { url = "https://files.pythonhosted.org/packages/a7/a0/f294cff6d1034b87839987e5b6ac7385bec599c44d08e0857ac7f164ad0c/coverage-7.10.5-cp314-cp314t-win_amd64.whl", hash = "sha256:0666cf3d2c1626b5a3463fd5b05f5e21f99e6aec40a3192eee4d07a15970b07f", size = 221714, upload-time = "2025-08-23T14:42:23.616Z" }, + { url = "https://files.pythonhosted.org/packages/23/18/fa1afdc60b5528d17416df440bcbd8fd12da12bfea9da5b6ae0f7a37d0f7/coverage-7.10.5-cp314-cp314t-win_arm64.whl", hash = "sha256:bc85eb2d35e760120540afddd3044a5bf69118a91a296a8b3940dfc4fdcfe1e2", size = 219735, upload-time = "2025-08-23T14:42:25.156Z" }, + { url = "https://files.pythonhosted.org/packages/3b/21/05248e8bc74683488cb7477e6b6b878decadd15af0ec96f56381d3d7ff2d/coverage-7.10.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:62835c1b00c4a4ace24c1a88561a5a59b612fbb83a525d1c70ff5720c97c0610", size = 216763, upload-time = "2025-08-23T14:42:26.75Z" }, + { url = "https://files.pythonhosted.org/packages/a9/7f/161a0ad40cb1c7e19dc1aae106d3430cc88dac3d651796d6cf3f3730c800/coverage-7.10.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5255b3bbcc1d32a4069d6403820ac8e6dbcc1d68cb28a60a1ebf17e47028e898", size = 217154, upload-time = "2025-08-23T14:42:28.238Z" }, + { url = "https://files.pythonhosted.org/packages/de/31/41929ee53af829ea5a88e71d335ea09d0bb587a3da1c5e58e59b48473ed8/coverage-7.10.5-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3876385722e335d6e991c430302c24251ef9c2a9701b2b390f5473199b1b8ebf", size = 243588, upload-time = "2025-08-23T14:42:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/6e/4e/2649344e33eeb3567041e8255a1942173cae81817fe06b60f3fafaafe111/coverage-7.10.5-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8048ce4b149c93447a55d279078c8ae98b08a6951a3c4d2d7e87f4efc7bfe100", size = 245412, upload-time = "2025-08-23T14:42:31.296Z" }, + { url = "https://files.pythonhosted.org/packages/ac/b1/b21e1e69986ad89b051dd42c3ef06d9326e03ac3c0c844fc33385d1d9e35/coverage-7.10.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4028e7558e268dd8bcf4d9484aad393cafa654c24b4885f6f9474bf53183a82a", size = 247182, upload-time = "2025-08-23T14:42:33.155Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b5/80837be411ae092e03fcc2a7877bd9a659c531eff50453e463057a9eee44/coverage-7.10.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:03f47dc870eec0367fcdd603ca6a01517d2504e83dc18dbfafae37faec66129a", size = 245066, upload-time = "2025-08-23T14:42:34.754Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ed/fcb0838ddf149d68d09f89af57397b0dd9d26b100cc729daf1b0caf0b2d3/coverage-7.10.5-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2d488d7d42b6ded7ea0704884f89dcabd2619505457de8fc9a6011c62106f6e5", size = 243138, upload-time = "2025-08-23T14:42:36.311Z" }, + { url = "https://files.pythonhosted.org/packages/75/0f/505c6af24a9ae5d8919d209b9c31b7092815f468fa43bec3b1118232c62a/coverage-7.10.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b3dcf2ead47fa8be14224ee817dfc1df98043af568fe120a22f81c0eb3c34ad2", size = 244095, upload-time = "2025-08-23T14:42:38.227Z" }, + { url = "https://files.pythonhosted.org/packages/e4/7e/c82a8bede46217c1d944bd19b65e7106633b998640f00ab49c5f747a5844/coverage-7.10.5-cp39-cp39-win32.whl", hash = "sha256:02650a11324b80057b8c9c29487020073d5e98a498f1857f37e3f9b6ea1b2426", size = 219289, upload-time = "2025-08-23T14:42:39.827Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ac/46645ef6be543f2e7de08cc2601a0b67e130c816be3b749ab741be689fb9/coverage-7.10.5-cp39-cp39-win_amd64.whl", hash = "sha256:b45264dd450a10f9e03237b41a9a24e85cbb1e278e5a32adb1a303f58f0017f3", size = 220199, upload-time = "2025-08-23T14:42:41.363Z" }, + { url = "https://files.pythonhosted.org/packages/08/b6/fff6609354deba9aeec466e4bcaeb9d1ed3e5d60b14b57df2a36fb2273f2/coverage-7.10.5-py3-none-any.whl", hash = "sha256:0be24d35e4db1d23d0db5c0f6a74a962e2ec83c426b5cac09f4234aadef38e4a", size = 208736, upload-time = "2025-08-23T14:42:43.145Z" }, ] [package.optional-dependencies] @@ -262,82 +318,60 @@ wheels = [ [[package]] name = "gitpython" -version = "3.1.44" +version = "3.1.45" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/89/37df0b71473153574a5cdef8f242de422a0f5d26d7a9e231e6f169b4ad14/gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269", size = 214196, upload-time = "2025-01-02T07:32:43.59Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076, upload-time = "2025-07-24T03:45:54.871Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599, upload-time = "2025-01-02T07:32:40.731Z" }, + { url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168, upload-time = "2025-07-24T03:45:52.517Z" }, ] [[package]] name = "griffe" -version = "1.6.1" +version = "1.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6a/ba/1ebe51a22c491a3fc94b44ef9c46a5b5472540e24a5c3f251cebbab7214b/griffe-1.6.1.tar.gz", hash = "sha256:ff0acf706b2680f8c721412623091c891e752b2c61b7037618f7b77d06732cf5", size = 393112, upload-time = "2025-03-18T15:18:45.489Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/b5/23b91f22b7b3a7f8f62223f6664946271c0f5cb4179605a3e6bbae863920/griffe-1.13.0.tar.gz", hash = "sha256:246ea436a5e78f7fbf5f24ca8a727bb4d2a4b442a2959052eea3d0bfe9a076e0", size = 412759, upload-time = "2025-08-26T13:27:11.422Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/d3/a760d1062e44587230aa65573c70edaad4ee8a0e60e193a3172b304d24d8/griffe-1.6.1-py3-none-any.whl", hash = "sha256:b0131670db16834f82383bcf4f788778853c9bf4dc7a1a2b708bb0808ca56a98", size = 128615, upload-time = "2025-03-18T15:18:43.57Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8c/b7cfdd8dfe48f6b09f7353323732e1a290c388bd14f216947928dc85f904/griffe-1.13.0-py3-none-any.whl", hash = "sha256:470fde5b735625ac0a36296cd194617f039e9e83e301fcbd493e2b58382d0559", size = 139365, upload-time = "2025-08-26T13:27:09.882Z" }, ] [[package]] name = "griffe-fieldz" -version = "0.2.1" +version = "0.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fieldz" }, { name = "griffe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/95/5c47b6c54d106c76aad9b51177401ad4215a0de910c6853b689a2c6d07da/griffe_fieldz-0.2.1.tar.gz", hash = "sha256:7371693cdd045ac95aaebd16a81586d8dfa72c30e2d4519e0dde0d80f8ef0dc7", size = 8148, upload-time = "2025-01-12T20:55:09.063Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/6a/94754bf39fd63ba424c667b2abf0ade78e3878e223591d1fb9c3e8a77bce/griffe_fieldz-0.3.0.tar.gz", hash = "sha256:42e7707dac51d38e26fb7f3f7f51429da9b47e98060bfeb81a4287456d5b8a89", size = 10149, upload-time = "2025-07-30T21:43:10.042Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/fc/c059d128d77a369f54a3753162a38cd7668e5b4d14464672001a50cf2e9d/griffe_fieldz-0.2.1-py3-none-any.whl", hash = "sha256:04ae78b487c832a38b0495f971784d513da413b867c51e429f39d74f76d4f941", size = 5691, upload-time = "2025-01-12T20:55:07.634Z" }, + { url = "https://files.pythonhosted.org/packages/4d/33/cc527c11132a6274724a04938d50e1ff2b54a5f5943cd0480427571e1adb/griffe_fieldz-0.3.0-py3-none-any.whl", hash = "sha256:52e02fdcbdf6dea3c8c95756d1e0b30861569f871d19437fda702776fde4e64d", size = 6577, upload-time = "2025-07-30T21:43:09.073Z" }, ] [[package]] name = "harp-protocol" -version = "0.2.0a2" -source = { editable = "." } +version = "0.3.0" +source = { editable = "src/harp-protocol" } + +[[package]] +name = "harp-serial" +version = "0.3.0" +source = { editable = "src/harp-serial" } dependencies = [ + { name = "harp-protocol" }, { name = "pyserial" }, ] -[package.dev-dependencies] -dev = [ - { name = "griffe-fieldz" }, - { name = "mkdocs" }, - { name = "mkdocs-codeinclude-plugin" }, - { name = "mkdocs-git-authors-plugin" }, - { name = "mkdocs-git-committers-plugin-2" }, - { name = "mkdocs-include-markdown-plugin" }, - { name = "mkdocs-material" }, - { name = "mkdocs-monorepo-plugin" }, - { name = "mkdocstrings-python" }, - { name = "pytest" }, - { name = "pytest-cov" }, - { name = "ruff" }, -] - [package.metadata] -requires-dist = [{ name = "pyserial", specifier = ">=3.5" }] - -[package.metadata.requires-dev] -dev = [ - { name = "griffe-fieldz", specifier = ">=0.2.1" }, - { name = "mkdocs", specifier = ">=1.6.1" }, - { name = "mkdocs-codeinclude-plugin", specifier = ">=0.2.1" }, - { name = "mkdocs-git-authors-plugin", specifier = ">=0.9.4" }, - { name = "mkdocs-git-committers-plugin-2", specifier = ">=2.5.0" }, - { name = "mkdocs-include-markdown-plugin", specifier = ">=7.1.6" }, - { name = "mkdocs-material", specifier = ">=9.6.9" }, - { name = "mkdocs-monorepo-plugin", specifier = ">=1.1.2" }, - { name = "mkdocstrings-python", specifier = ">=1.16.6" }, - { name = "pytest", specifier = ">=8.3.5" }, - { name = "pytest-cov", specifier = ">=6.1.1" }, - { name = "ruff", specifier = ">=0.11.0" }, +requires-dist = [ + { name = "harp-protocol", editable = "src/harp-protocol" }, + { name = "pyserial", specifier = ">=3.5" }, ] [[package]] @@ -363,11 +397,11 @@ wheels = [ [[package]] name = "iniconfig" -version = "2.0.0" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] [[package]] @@ -384,14 +418,14 @@ wheels = [ [[package]] name = "markdown" -version = "3.7" +version = "3.8.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086, upload-time = "2024-08-16T15:55:17.812Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071, upload-time = "2025-06-19T17:12:44.483Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349, upload-time = "2024-08-16T15:55:16.176Z" }, + { url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827, upload-time = "2025-06-19T17:12:42.994Z" }, ] [[package]] @@ -476,7 +510,8 @@ name = "mkdocs" version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click" }, + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "ghp-import" }, { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, @@ -498,16 +533,16 @@ wheels = [ [[package]] name = "mkdocs-autorefs" -version = "1.4.1" +version = "1.4.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "markupsafe" }, { name = "mkdocs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/44/140469d87379c02f1e1870315f3143718036a983dd0416650827b8883192/mkdocs_autorefs-1.4.1.tar.gz", hash = "sha256:4b5b6235a4becb2b10425c2fa191737e415b37aa3418919db33e5d774c9db079", size = 4131355, upload-time = "2025-03-08T13:35:21.232Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/fa/9124cd63d822e2bcbea1450ae68cdc3faf3655c69b455f3a7ed36ce6c628/mkdocs_autorefs-1.4.3.tar.gz", hash = "sha256:beee715b254455c4aa93b6ef3c67579c399ca092259cc41b7d9342573ff1fc75", size = 55425, upload-time = "2025-08-26T14:23:17.223Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/29/1125f7b11db63e8e32bcfa0752a4eea30abff3ebd0796f808e14571ddaa2/mkdocs_autorefs-1.4.1-py3-none-any.whl", hash = "sha256:9793c5ac06a6ebbe52ec0f8439256e66187badf4b5334b5fde0b128ec134df4f", size = 5782047, upload-time = "2025-03-08T13:35:18.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/4d/7123b6fa2278000688ebd338e2a06d16870aaf9eceae6ba047ea05f92df1/mkdocs_autorefs-1.4.3-py3-none-any.whl", hash = "sha256:469d85eb3114801d08e9cc55d102b3ba65917a869b893403b8987b601cf55dc9", size = 25034, upload-time = "2025-08-26T14:23:15.906Z" }, ] [[package]] @@ -540,14 +575,14 @@ wheels = [ [[package]] name = "mkdocs-git-authors-plugin" -version = "0.9.4" +version = "0.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mkdocs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/87/9a/063c4a3688e4669eb2054e4bf6e9cc582f6c1d85674e3f5b836ceff97c3b/mkdocs_git_authors_plugin-0.9.4.tar.gz", hash = "sha256:f5cfaf93d08981ce25591bbaf642051ed168c3886bb96ecd2dca53f0ef1973b8", size = 21914, upload-time = "2025-03-14T19:26:40.873Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/f1/b784c631b812aab80030db80127a576b68a84caac5229836fb7fcc00e055/mkdocs_git_authors_plugin-0.10.0.tar.gz", hash = "sha256:29d1973b2835663d79986fb756e02f1f0ff3fe35c278e993206bd3c550c205e4", size = 23432, upload-time = "2025-06-10T05:42:40.94Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/ac/2b5bae4047276fda2bdd14a6d4af59288fb4d5de54151ae4e6ba17611ceb/mkdocs_git_authors_plugin-0.9.4-py3-none-any.whl", hash = "sha256:84b9b56c703841189c64d8ff6947034fe0a9c14a0a8f1f6255edfcfe3a56825f", size = 20752, upload-time = "2025-03-14T19:26:39.398Z" }, + { url = "https://files.pythonhosted.org/packages/41/bc/a4166201c2789657c4d370bfcd71a5107edec185ae245675c8b9a6719243/mkdocs_git_authors_plugin-0.10.0-py3-none-any.whl", hash = "sha256:28421a99c3e872a8e205674bb80ec48524838243e5f59eaf9bd97df103e38901", size = 21899, upload-time = "2025-06-10T05:42:39.244Z" }, ] [[package]] @@ -579,11 +614,13 @@ wheels = [ [[package]] name = "mkdocs-material" -version = "9.6.9" +version = "9.6.18" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, { name = "backrefs" }, + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "colorama" }, { name = "jinja2" }, { name = "markdown" }, @@ -594,9 +631,9 @@ dependencies = [ { name = "pymdown-extensions" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/11/cb/6dd3b6a7925429c0229738098ee874dbf7fa02db55558adb2c5bf86077b2/mkdocs_material-9.6.9.tar.gz", hash = "sha256:a4872139715a1f27b2aa3f3dc31a9794b7bbf36333c0ba4607cf04786c94f89c", size = 3948083, upload-time = "2025-03-17T09:28:50.879Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/46/db0d78add5aac29dfcd0a593bcc6049c86c77ba8a25b3a5b681c190d5e99/mkdocs_material-9.6.18.tar.gz", hash = "sha256:a2eb253bcc8b66f8c6eaf8379c10ed6e9644090c2e2e9d0971c7722dc7211c05", size = 4034856, upload-time = "2025-08-22T08:21:47.575Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/7c/ea5a671b2ff5d0e3f3108a7f7d75b541d683e4969aaead2a8f3e59e0fc27/mkdocs_material-9.6.9-py3-none-any.whl", hash = "sha256:6e61b7fb623ce2aa4622056592b155a9eea56ff3487d0835075360be45a4c8d1", size = 8697935, upload-time = "2025-03-17T09:28:47.481Z" }, + { url = "https://files.pythonhosted.org/packages/22/0b/545a4f8d4f9057e77f1d99640eb09aaae40c4f9034707f25636caf716ff9/mkdocs_material-9.6.18-py3-none-any.whl", hash = "sha256:dbc1e146a0ecce951a4d84f97b816a54936cdc9e1edd1667fc6868878ac06701", size = 9232642, upload-time = "2025-08-22T08:21:44.52Z" }, ] [[package]] @@ -623,7 +660,7 @@ wheels = [ [[package]] name = "mkdocstrings" -version = "0.29.0" +version = "0.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, @@ -633,16 +670,15 @@ dependencies = [ { name = "mkdocs" }, { name = "mkdocs-autorefs" }, { name = "pymdown-extensions" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8e/4d/a9484dc5d926295bdf308f1f6c4f07fcc99735b970591edc414d401fcc91/mkdocstrings-0.29.0.tar.gz", hash = "sha256:3657be1384543ce0ee82112c3e521bbf48e41303aa0c229b9ffcccba057d922e", size = 1212185, upload-time = "2025-03-10T13:10:11.445Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/0a/7e4776217d4802009c8238c75c5345e23014a4706a8414a62c0498858183/mkdocstrings-0.30.0.tar.gz", hash = "sha256:5d8019b9c31ddacd780b6784ffcdd6f21c408f34c0bd1103b5351d609d5b4444", size = 106597, upload-time = "2025-07-22T23:48:45.998Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/47/eb876dfd84e48f31ff60897d161b309cf6a04ca270155b0662aae562b3fb/mkdocstrings-0.29.0-py3-none-any.whl", hash = "sha256:8ea98358d2006f60befa940fdebbbc88a26b37ecbcded10be726ba359284f73d", size = 1630824, upload-time = "2025-03-10T13:10:09.712Z" }, + { url = "https://files.pythonhosted.org/packages/de/b4/3c5eac68f31e124a55d255d318c7445840fa1be55e013f507556d6481913/mkdocstrings-0.30.0-py3-none-any.whl", hash = "sha256:ae9e4a0d8c1789697ac776f2e034e2ddd71054ae1cf2c2bb1433ccfd07c226f2", size = 36579, upload-time = "2025-07-22T23:48:44.152Z" }, ] [[package]] name = "mkdocstrings-python" -version = "1.16.6" +version = "1.18.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "griffe" }, @@ -650,18 +686,18 @@ dependencies = [ { name = "mkdocstrings" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8e/e7/0691e34e807a8f5c28f0988fcfeeb584f0b569ce433bf341944f14bdb3ff/mkdocstrings_python-1.16.6.tar.gz", hash = "sha256:cefe0f0e17ab4a4611f01b0a2af75e4298664e0ff54feb83c91a485bfed82dc9", size = 201565, upload-time = "2025-03-18T15:34:24.371Z" } +sdist = { url = "https://files.pythonhosted.org/packages/13/d4/6327c4e82dda667b0ff83b6f6b6a03e7b81dfd1f28cd5eda50ffe66d546f/mkdocstrings_python-1.18.0.tar.gz", hash = "sha256:0b9924b4034fe9ae43604d78fe8e5107ea2c2391620124fc833043a62e83c744", size = 207601, upload-time = "2025-08-26T14:02:30.839Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/42/ed682687ef5f248e104f82806d5d9893f6dd81d8cb4561692e190ba1a252/mkdocstrings_python-1.16.6-py3-none-any.whl", hash = "sha256:de877dd71f69878c973c4897a39683b7b6961bee7b058879095b69681488453f", size = 123207, upload-time = "2025-03-18T15:34:21.357Z" }, + { url = "https://files.pythonhosted.org/packages/5d/96/7ecc71bb9f01ee20f201b2531960b401159c6730aec90ec76a1b74bc81e1/mkdocstrings_python-1.18.0-py3-none-any.whl", hash = "sha256:f5056d8afe9a9683ad0c59001df1ecd9668b51c19b9a6b4dc0ff02cc9b76265a", size = 138182, upload-time = "2025-08-26T14:02:28.076Z" }, ] [[package]] name = "packaging" -version = "24.2" +version = "25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950, upload-time = "2024-11-08T09:47:47.202Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload-time = "2024-11-08T09:47:44.722Z" }, + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] [[package]] @@ -684,42 +720,89 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.3.6" +version = "4.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302, upload-time = "2024-09-17T19:06:50.688Z" } +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439, upload-time = "2024-09-17T19:06:49.212Z" }, + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, ] [[package]] name = "pluggy" -version = "1.5.0" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "pygments" -version = "2.19.1" +version = "2.19.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyharp" +version = "0.2.0" +source = { virtual = "." } +dependencies = [ + { name = "harp-protocol" }, + { name = "harp-serial" }, +] + +[package.dev-dependencies] +dev = [ + { name = "griffe-fieldz" }, + { name = "mkdocs" }, + { name = "mkdocs-codeinclude-plugin" }, + { name = "mkdocs-git-authors-plugin" }, + { name = "mkdocs-git-committers-plugin-2" }, + { name = "mkdocs-include-markdown-plugin" }, + { name = "mkdocs-material" }, + { name = "mkdocs-monorepo-plugin" }, + { name = "mkdocstrings-python" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "harp-protocol", editable = "src/harp-protocol" }, + { name = "harp-serial", editable = "src/harp-serial" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "griffe-fieldz", specifier = ">=0.2.1" }, + { name = "mkdocs", specifier = ">=1.6.1" }, + { name = "mkdocs-codeinclude-plugin", specifier = ">=0.2.1" }, + { name = "mkdocs-git-authors-plugin", specifier = ">=0.9.4" }, + { name = "mkdocs-git-committers-plugin-2", specifier = ">=2.5.0" }, + { name = "mkdocs-include-markdown-plugin", specifier = ">=7.1.6" }, + { name = "mkdocs-material", specifier = ">=9.6.9" }, + { name = "mkdocs-monorepo-plugin", specifier = ">=1.1.2" }, + { name = "mkdocstrings-python", specifier = ">=1.16.6" }, + { name = "pytest", specifier = ">=8.3.5" }, + { name = "pytest-cov", specifier = ">=6.1.1" }, + { name = "ruff", specifier = ">=0.11.0" }, ] [[package]] name = "pymdown-extensions" -version = "10.14.3" +version = "10.16.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7c/44/e6de2fdc880ad0ec7547ca2e087212be815efbc9a425a8d5ba9ede602cbb/pymdown_extensions-10.14.3.tar.gz", hash = "sha256:41e576ce3f5d650be59e900e4ceff231e0aed2a88cf30acaee41e02f063a061b", size = 846846, upload-time = "2025-02-01T15:43:15.42Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/b3/6d2b3f149bc5413b0a29761c2c5832d8ce904a1d7f621e86616d96f505cc/pymdown_extensions-10.16.1.tar.gz", hash = "sha256:aace82bcccba3efc03e25d584e6a22d27a8e17caa3f4dd9f207e49b787aa9a91", size = 853277, upload-time = "2025-07-28T16:19:34.167Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/f5/b9e2a42aa8f9e34d52d66de87941ecd236570c7ed2e87775ed23bbe4e224/pymdown_extensions-10.14.3-py3-none-any.whl", hash = "sha256:05e0bee73d64b9c71a4ae17c72abc2f700e8bc8403755a00580b49a4e9f189e9", size = 264467, upload-time = "2025-02-01T15:43:13.995Z" }, + { url = "https://files.pythonhosted.org/packages/e4/06/43084e6cbd4b3bc0e80f6be743b2e79fbc6eed8de9ad8c629939fa55d972/pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d", size = 266178, upload-time = "2025-07-28T16:19:31.401Z" }, ] [[package]] @@ -733,7 +816,7 @@ wheels = [ [[package]] name = "pytest" -version = "8.3.5" +version = "8.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -741,24 +824,26 @@ dependencies = [ { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, + { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, ] [[package]] name = "pytest-cov" -version = "6.1.1" +version = "6.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857, upload-time = "2025-04-05T14:07:51.592Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841, upload-time = "2025-04-05T14:07:49.641Z" }, + { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" }, ] [[package]] @@ -840,19 +925,19 @@ wheels = [ [[package]] name = "pyyaml-env-tag" -version = "0.1" +version = "1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/8e/da1c6c58f751b70f8ceb1eb25bc25d524e8f14fe16edcce3f4e3ba08629c/pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb", size = 5631, upload-time = "2020-11-12T02:38:26.239Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/66/bbb1dd374f5c870f59c5bb1db0e18cbe7fa739415a24cbd95b2d1f5ae0c4/pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069", size = 3911, upload-time = "2020-11-12T02:38:24.638Z" }, + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, ] [[package]] name = "requests" -version = "2.32.3" +version = "2.32.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -860,34 +945,35 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] [[package]] name = "ruff" -version = "0.11.0" +version = "0.12.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/77/2b/7ca27e854d92df5e681e6527dc0f9254c9dc06c8408317893cf96c851cdd/ruff-0.11.0.tar.gz", hash = "sha256:e55c620690a4a7ee6f1cccb256ec2157dc597d109400ae75bbf944fc9d6462e2", size = 3799407, upload-time = "2025-03-14T13:52:36.539Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/eb/8c073deb376e46ae767f4961390d17545e8535921d2f65101720ed8bd434/ruff-0.12.10.tar.gz", hash = "sha256:189ab65149d11ea69a2d775343adf5f49bb2426fc4780f65ee33b423ad2e47f9", size = 5310076, upload-time = "2025-08-21T18:23:22.595Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/40/3d0340a9e5edc77d37852c0cd98c5985a5a8081fc3befaeb2ae90aaafd2b/ruff-0.11.0-py3-none-linux_armv6l.whl", hash = "sha256:dc67e32bc3b29557513eb7eeabb23efdb25753684b913bebb8a0c62495095acb", size = 10098158, upload-time = "2025-03-14T13:51:55.69Z" }, - { url = "https://files.pythonhosted.org/packages/ec/a9/d8f5abb3b87b973b007649ac7bf63665a05b2ae2b2af39217b09f52abbbf/ruff-0.11.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38c23fd9bdec4eb437b4c1e3595905a0a8edfccd63a790f818b28c78fe345639", size = 10879071, upload-time = "2025-03-14T13:51:58.989Z" }, - { url = "https://files.pythonhosted.org/packages/ab/62/aaa198614c6211677913ec480415c5e6509586d7b796356cec73a2f8a3e6/ruff-0.11.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7c8661b0be91a38bd56db593e9331beaf9064a79028adee2d5f392674bbc5e88", size = 10247944, upload-time = "2025-03-14T13:52:02.318Z" }, - { url = "https://files.pythonhosted.org/packages/9f/52/59e0a9f2cf1ce5e6cbe336b6dd0144725c8ea3b97cac60688f4e7880bf13/ruff-0.11.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6c0e8d3d2db7e9f6efd884f44b8dc542d5b6b590fc4bb334fdbc624d93a29a2", size = 10421725, upload-time = "2025-03-14T13:52:04.303Z" }, - { url = "https://files.pythonhosted.org/packages/a6/c3/dcd71acc6dff72ce66d13f4be5bca1dbed4db678dff2f0f6f307b04e5c02/ruff-0.11.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c3156d3f4b42e57247275a0a7e15a851c165a4fc89c5e8fa30ea6da4f7407b8", size = 9954435, upload-time = "2025-03-14T13:52:06.602Z" }, - { url = "https://files.pythonhosted.org/packages/a6/9a/342d336c7c52dbd136dee97d4c7797e66c3f92df804f8f3b30da59b92e9c/ruff-0.11.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:490b1e147c1260545f6d041c4092483e3f6d8eba81dc2875eaebcf9140b53905", size = 11492664, upload-time = "2025-03-14T13:52:08.613Z" }, - { url = "https://files.pythonhosted.org/packages/84/35/6e7defd2d7ca95cc385ac1bd9f7f2e4a61b9cc35d60a263aebc8e590c462/ruff-0.11.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1bc09a7419e09662983b1312f6fa5dab829d6ab5d11f18c3760be7ca521c9329", size = 12207856, upload-time = "2025-03-14T13:52:11.019Z" }, - { url = "https://files.pythonhosted.org/packages/22/78/da669c8731bacf40001c880ada6d31bcfb81f89cc996230c3b80d319993e/ruff-0.11.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcfa478daf61ac8002214eb2ca5f3e9365048506a9d52b11bea3ecea822bb844", size = 11645156, upload-time = "2025-03-14T13:52:13.383Z" }, - { url = "https://files.pythonhosted.org/packages/ee/47/e27d17d83530a208f4a9ab2e94f758574a04c51e492aa58f91a3ed7cbbcb/ruff-0.11.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6fbb2aed66fe742a6a3a0075ed467a459b7cedc5ae01008340075909d819df1e", size = 13884167, upload-time = "2025-03-14T13:52:15.528Z" }, - { url = "https://files.pythonhosted.org/packages/9f/5e/42ffbb0a5d4b07bbc642b7d58357b4e19a0f4774275ca6ca7d1f7b5452cd/ruff-0.11.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92c0c1ff014351c0b0cdfdb1e35fa83b780f1e065667167bb9502d47ca41e6db", size = 11348311, upload-time = "2025-03-14T13:52:18.088Z" }, - { url = "https://files.pythonhosted.org/packages/c8/51/dc3ce0c5ce1a586727a3444a32f98b83ba99599bb1ebca29d9302886e87f/ruff-0.11.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e4fd5ff5de5f83e0458a138e8a869c7c5e907541aec32b707f57cf9a5e124445", size = 10305039, upload-time = "2025-03-14T13:52:20.476Z" }, - { url = "https://files.pythonhosted.org/packages/60/e0/475f0c2f26280f46f2d6d1df1ba96b3399e0234cf368cc4c88e6ad10dcd9/ruff-0.11.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:96bc89a5c5fd21a04939773f9e0e276308be0935de06845110f43fd5c2e4ead7", size = 9937939, upload-time = "2025-03-14T13:52:22.798Z" }, - { url = "https://files.pythonhosted.org/packages/e2/d3/3e61b7fd3e9cdd1e5b8c7ac188bec12975c824e51c5cd3d64caf81b0331e/ruff-0.11.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a9352b9d767889ec5df1483f94870564e8102d4d7e99da52ebf564b882cdc2c7", size = 10923259, upload-time = "2025-03-14T13:52:24.89Z" }, - { url = "https://files.pythonhosted.org/packages/30/32/cd74149ebb40b62ddd14bd2d1842149aeb7f74191fb0f49bd45c76909ff2/ruff-0.11.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:049a191969a10897fe052ef9cc7491b3ef6de79acd7790af7d7897b7a9bfbcb6", size = 11406212, upload-time = "2025-03-14T13:52:27.493Z" }, - { url = "https://files.pythonhosted.org/packages/00/ef/033022a6b104be32e899b00de704d7c6d1723a54d4c9e09d147368f14b62/ruff-0.11.0-py3-none-win32.whl", hash = "sha256:3191e9116b6b5bbe187447656f0c8526f0d36b6fd89ad78ccaad6bdc2fad7df2", size = 10310905, upload-time = "2025-03-14T13:52:30.46Z" }, - { url = "https://files.pythonhosted.org/packages/ed/8a/163f2e78c37757d035bd56cd60c8d96312904ca4a6deeab8442d7b3cbf89/ruff-0.11.0-py3-none-win_amd64.whl", hash = "sha256:c58bfa00e740ca0a6c43d41fb004cd22d165302f360aaa56f7126d544db31a21", size = 11411730, upload-time = "2025-03-14T13:52:32.508Z" }, - { url = "https://files.pythonhosted.org/packages/4e/f7/096f6efabe69b49d7ca61052fc70289c05d8d35735c137ef5ba5ef423662/ruff-0.11.0-py3-none-win_arm64.whl", hash = "sha256:868364fc23f5aa122b00c6f794211e85f7e78f5dffdf7c590ab90b8c4e69b657", size = 10538956, upload-time = "2025-03-14T13:52:34.491Z" }, + { url = "https://files.pythonhosted.org/packages/24/e7/560d049d15585d6c201f9eeacd2fd130def3741323e5ccf123786e0e3c95/ruff-0.12.10-py3-none-linux_armv6l.whl", hash = "sha256:8b593cb0fb55cc8692dac7b06deb29afda78c721c7ccfed22db941201b7b8f7b", size = 11935161, upload-time = "2025-08-21T18:22:26.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b0/ad2464922a1113c365d12b8f80ed70fcfb39764288ac77c995156080488d/ruff-0.12.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ebb7333a45d56efc7c110a46a69a1b32365d5c5161e7244aaf3aa20ce62399c1", size = 12660884, upload-time = "2025-08-21T18:22:30.925Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f1/97f509b4108d7bae16c48389f54f005b62ce86712120fd8b2d8e88a7cb49/ruff-0.12.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d59e58586829f8e4a9920788f6efba97a13d1fa320b047814e8afede381c6839", size = 11872754, upload-time = "2025-08-21T18:22:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/12/ad/44f606d243f744a75adc432275217296095101f83f966842063d78eee2d3/ruff-0.12.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:822d9677b560f1fdeab69b89d1f444bf5459da4aa04e06e766cf0121771ab844", size = 12092276, upload-time = "2025-08-21T18:22:36.764Z" }, + { url = "https://files.pythonhosted.org/packages/06/1f/ed6c265e199568010197909b25c896d66e4ef2c5e1c3808caf461f6f3579/ruff-0.12.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:37b4a64f4062a50c75019c61c7017ff598cb444984b638511f48539d3a1c98db", size = 11734700, upload-time = "2025-08-21T18:22:39.822Z" }, + { url = "https://files.pythonhosted.org/packages/63/c5/b21cde720f54a1d1db71538c0bc9b73dee4b563a7dd7d2e404914904d7f5/ruff-0.12.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c6f4064c69d2542029b2a61d39920c85240c39837599d7f2e32e80d36401d6e", size = 13468783, upload-time = "2025-08-21T18:22:42.559Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/39369e6ac7f2a1848f22fb0b00b690492f20811a1ac5c1fd1d2798329263/ruff-0.12.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:059e863ea3a9ade41407ad71c1de2badfbe01539117f38f763ba42a1206f7559", size = 14436642, upload-time = "2025-08-21T18:22:45.612Z" }, + { url = "https://files.pythonhosted.org/packages/e3/03/5da8cad4b0d5242a936eb203b58318016db44f5c5d351b07e3f5e211bb89/ruff-0.12.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bef6161e297c68908b7218fa6e0e93e99a286e5ed9653d4be71e687dff101cf", size = 13859107, upload-time = "2025-08-21T18:22:48.886Z" }, + { url = "https://files.pythonhosted.org/packages/19/19/dd7273b69bf7f93a070c9cec9494a94048325ad18fdcf50114f07e6bf417/ruff-0.12.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4f1345fbf8fb0531cd722285b5f15af49b2932742fc96b633e883da8d841896b", size = 12886521, upload-time = "2025-08-21T18:22:51.567Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1d/b4207ec35e7babaee62c462769e77457e26eb853fbdc877af29417033333/ruff-0.12.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f68433c4fbc63efbfa3ba5db31727db229fa4e61000f452c540474b03de52a9", size = 13097528, upload-time = "2025-08-21T18:22:54.609Z" }, + { url = "https://files.pythonhosted.org/packages/ff/00/58f7b873b21114456e880b75176af3490d7a2836033779ca42f50de3b47a/ruff-0.12.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:141ce3d88803c625257b8a6debf4a0473eb6eed9643a6189b68838b43e78165a", size = 13080443, upload-time = "2025-08-21T18:22:57.413Z" }, + { url = "https://files.pythonhosted.org/packages/12/8c/9e6660007fb10189ccb78a02b41691288038e51e4788bf49b0a60f740604/ruff-0.12.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f3fc21178cd44c98142ae7590f42ddcb587b8e09a3b849cbc84edb62ee95de60", size = 11896759, upload-time = "2025-08-21T18:23:00.473Z" }, + { url = "https://files.pythonhosted.org/packages/67/4c/6d092bb99ea9ea6ebda817a0e7ad886f42a58b4501a7e27cd97371d0ba54/ruff-0.12.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7d1a4e0bdfafcd2e3e235ecf50bf0176f74dd37902f241588ae1f6c827a36c56", size = 11701463, upload-time = "2025-08-21T18:23:03.211Z" }, + { url = "https://files.pythonhosted.org/packages/59/80/d982c55e91df981f3ab62559371380616c57ffd0172d96850280c2b04fa8/ruff-0.12.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e67d96827854f50b9e3e8327b031647e7bcc090dbe7bb11101a81a3a2cbf1cc9", size = 12691603, upload-time = "2025-08-21T18:23:06.935Z" }, + { url = "https://files.pythonhosted.org/packages/ad/37/63a9c788bbe0b0850611669ec6b8589838faf2f4f959647f2d3e320383ae/ruff-0.12.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ae479e1a18b439c59138f066ae79cc0f3ee250712a873d00dbafadaad9481e5b", size = 13164356, upload-time = "2025-08-21T18:23:10.225Z" }, + { url = "https://files.pythonhosted.org/packages/47/d4/1aaa7fb201a74181989970ebccd12f88c0fc074777027e2a21de5a90657e/ruff-0.12.10-py3-none-win32.whl", hash = "sha256:9de785e95dc2f09846c5e6e1d3a3d32ecd0b283a979898ad427a9be7be22b266", size = 11896089, upload-time = "2025-08-21T18:23:14.232Z" }, + { url = "https://files.pythonhosted.org/packages/ad/14/2ad38fd4037daab9e023456a4a40ed0154e9971f8d6aed41bdea390aabd9/ruff-0.12.10-py3-none-win_amd64.whl", hash = "sha256:7837eca8787f076f67aba2ca559cefd9c5cbc3a9852fd66186f4201b87c1563e", size = 13004616, upload-time = "2025-08-21T18:23:17.422Z" }, + { url = "https://files.pythonhosted.org/packages/24/3c/21cf283d67af33a8e6ed242396863af195a8a6134ec581524fd22b9811b6/ruff-0.12.10-py3-none-win_arm64.whl", hash = "sha256:cc138cc06ed9d4bfa9d667a65af7172b47840e1a98b02ce7011c391e54635ffc", size = 12074225, upload-time = "2025-08-21T18:23:20.137Z" }, ] [[package]] @@ -958,20 +1044,20 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.14.0" +version = "4.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] [[package]] name = "urllib3" -version = "2.3.0" +version = "2.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268, upload-time = "2024-12-22T07:47:30.032Z" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369, upload-time = "2024-12-22T07:47:28.074Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, ] [[package]] From 9073f4cc81bba6277dee1be094b65f1a34a3120b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 27 Aug 2025 15:23:22 +0100 Subject: [PATCH 144/267] Update docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- docs/index.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/index.md b/docs/index.md index 5dd1923..d50f918 100644 --- a/docs/index.md +++ b/docs/index.md @@ -14,8 +14,8 @@ pip install harp-protocol ```python from harp import MessageType, PayloadType -from harp.device import Device -from harp.messages import HarpMessage +from harp.serial import Device +from harp.protocol.messages import HarpMessage # Connect to a device device = Device("/dev/ttyUSB0") @@ -30,7 +30,7 @@ register_address = 32 value = device.send(HarpMessage.create(MessageType.READ, register_address, PayloadType.U8)) # Write to register -device.send(HarpMessage.create(MessageType.WRITE, register_address, PayloadType.U8, value)) +device.send(HarpMessage.create(MessageType.WRITE, register_address, PayloadType.U8, value.payload)) # Disconnect when done device.disconnect() @@ -40,8 +40,8 @@ or using the `with` statement: ```python from harp import MessageType, PayloadType -from harp.device import Device -from harp.messages import HarpMessage +from harp.serial import Device +from harp.protocol.messages import HarpMessage with Device("/dev/ttyUSB0") as device: # Get device information @@ -54,7 +54,7 @@ with Device("/dev/ttyUSB0") as device: value = device.send(HarpMessage.create(MessageType.READ, register_address, PayloadType.U8)) # Write to register - device.send(HarpMessage.create(MessageType.WRITE, register_address, PayloadType.U8, value)) + device.send(HarpMessage.create(MessageType.WRITE, register_address, PayloadType.U8, value.payload)) ``` ## for Linux From 8c07844983a30bc92443bde071859066089f7ecf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Grilo?= Date: Wed, 27 Aug 2025 17:25:04 +0100 Subject: [PATCH 145/267] Chore: add pyrefly extension to `extensions.json` --- .vscode/extensions.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 2faf153..dd4b3f2 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,6 +1,7 @@ { "recommendations": [ "ms-python.python", - "charliermarsh.ruff" + "charliermarsh.ruff", + "meta.pyrefly" ] } From ee5aa325160ba515853bc2f403ecb1c91af92990 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Grilo?= Date: Wed, 27 Aug 2025 17:25:39 +0100 Subject: [PATCH 146/267] Chore: add script to run docs locally on Windows --- run_docs.ps1 | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 run_docs.ps1 diff --git a/run_docs.ps1 b/run_docs.ps1 new file mode 100644 index 0000000..913404a --- /dev/null +++ b/run_docs.ps1 @@ -0,0 +1,31 @@ +# Resolve script directory (optional) and change to that directory if desired: +# Set-Location -Path (Split-Path -Path $MyInvocation.MyCommand.Definition -Parent) + +# Launch mkdocs via uv (argument handling) +param( + [string]$action = "" +) + +# Find all subdirectories of .\harp.devices (follow symlinks) +$harpRoot = Join-Path -Path (Get-Location) -ChildPath "harp.devices" +$dirs = Get-ChildItem -LiteralPath $harpRoot -Directory -Force -ErrorAction SilentlyContinue | + ForEach-Object { $_.FullName } + +# Join them with ':' (Unix-style path separator) +$harpDevicesPaths = ($dirs -join ";") + +# Prepend to PYTHONPATH (preserve existing) +if ($env:PYTHONPATH) { + $env:PYTHONPATH = "{0};{1}" -f $harpDevicesPaths, $env:PYTHONPATH +} else { + $env:PYTHONPATH = $harpDevicesPaths +} + +# Optionally print for debugging +Write-Output "PYTHONPATH set to: $env:PYTHONPATH" + +switch ($action) { + "build" { uv run mkdocs build } + "deploy" { uv run mkdocs gh-deploy } + default { uv run mkdocs serve } +} From 594399a7153e9a6cf2529d0812eedfe8f1537d8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Grilo?= Date: Wed, 27 Aug 2025 17:26:49 +0100 Subject: [PATCH 147/267] Add harp reference epoch constant --- src/harp-protocol/harp/protocol/base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/harp-protocol/harp/protocol/base.py b/src/harp-protocol/harp/protocol/base.py index 2217dcf..8282c93 100644 --- a/src/harp-protocol/harp/protocol/base.py +++ b/src/harp-protocol/harp/protocol/base.py @@ -1,5 +1,9 @@ +from datetime import datetime from enum import IntEnum, IntFlag +# The reference epoch for UTC harp time +REFERENCE_EPOCH = datetime(1904, 1, 1) + # Bit masks for the PayloadType _isUnsigned: int = 0x00 _isSigned: int = 0x80 From 93aa50c4e2f1c90d69e4e82b9ea3f22e9b899595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Grilo?= Date: Wed, 27 Aug 2025 17:27:34 +0100 Subject: [PATCH 148/267] Fix issues with types --- src/harp-serial/harp/serial/device.py | 51 ++++++++------------------- 1 file changed, 15 insertions(+), 36 deletions(-) diff --git a/src/harp-serial/harp/serial/device.py b/src/harp-serial/harp/serial/device.py index bdc554f..5c4063d 100644 --- a/src/harp-serial/harp/serial/device.py +++ b/src/harp-serial/harp/serial/device.py @@ -7,7 +7,6 @@ from pathlib import Path from typing import Optional -import serial from harp.protocol import ( ClockConfig, CommonRegisters, @@ -22,6 +21,8 @@ from harp.protocol.messages import HarpMessage, ReplyHarpMessage from harp.serial.harp_serial import HarpSerial +import serial + class TimeoutStrategy(Enum): RAISE = "raise" # Raise HarpTimeoutError @@ -100,7 +101,7 @@ def __init__( """ self.log = logging.getLogger(f"{__name__}.{self.__class__.__name__}") self._serial_port = serial_port - self._dump_file_path = dump_file_path + self._dump_file_path = None if dump_file_path is not None: self._dump_file_path = Path() / dump_file_path self._read_timeout_s = read_timeout_s @@ -1152,7 +1153,7 @@ def _read_who_am_i(self) -> int: """ address = CommonRegisters.WHO_AM_I - reply: ReplyHarpMessage = self.send( + reply = self.send( HarpMessage.create(MessageType.READ, address, PayloadType.U16) ) @@ -1180,9 +1181,7 @@ def _read_hw_version_h(self) -> int: """ address = CommonRegisters.HW_VERSION_H - reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8) - ) + reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8)) return reply.payload @@ -1197,9 +1196,7 @@ def _read_hw_version_l(self) -> int: """ address = CommonRegisters.HW_VERSION_L - reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8) - ) + reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8)) return reply.payload @@ -1214,9 +1211,7 @@ def _read_assembly_version(self) -> int: """ address = CommonRegisters.ASSEMBLY_VERSION - reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8) - ) + reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8)) return reply.payload @@ -1231,9 +1226,7 @@ def _read_harp_version_h(self) -> int: """ address = CommonRegisters.HARP_VERSION_H - reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8) - ) + reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8)) return reply.payload @@ -1248,9 +1241,7 @@ def _read_harp_version_l(self) -> int: """ address = CommonRegisters.HARP_VERSION_L - reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8) - ) + reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8)) return reply.payload @@ -1265,9 +1256,7 @@ def _read_fw_version_h(self) -> int: """ address = CommonRegisters.FIRMWARE_VERSION_H - reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8) - ) + reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8)) return reply.payload @@ -1282,9 +1271,7 @@ def _read_fw_version_l(self) -> int: """ address = CommonRegisters.FIRMWARE_VERSION_L - reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8) - ) + reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8)) return reply.payload @@ -1299,9 +1286,7 @@ def _read_device_name(self) -> str: """ address = CommonRegisters.DEVICE_NAME - reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8) - ) + reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8)) return reply.payload_as_string() @@ -1316,9 +1301,7 @@ def _read_serial_number(self) -> int: """ address = CommonRegisters.SERIAL_NUMBER - reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8) - ) + reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8)) if reply.is_error: return 0 @@ -1336,9 +1319,7 @@ def _read_clock_config(self) -> int: """ address = CommonRegisters.CLOCK_CONFIG - reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8) - ) + reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8)) return reply.payload @@ -1353,9 +1334,7 @@ def _read_timestamp_offset(self) -> int: """ address = CommonRegisters.TIMESTAMP_OFFSET - reply: ReplyHarpMessage = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8) - ) + reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8)) return reply.payload From 2481ec1f537a6f13788bce1ed6909f0b4abc26cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Grilo?= Date: Wed, 27 Aug 2025 17:28:58 +0100 Subject: [PATCH 149/267] Docs: fix imports from examples to take the new project structure into account --- docs/examples/olfactometer_example/olfactometer_example.py | 5 ++--- .../read_and_write_from_registers.py | 5 ++--- docs/examples/wait_for_events/wait_for_events.py | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/examples/olfactometer_example/olfactometer_example.py b/docs/examples/olfactometer_example/olfactometer_example.py index 6078a77..b4ac526 100644 --- a/docs/examples/olfactometer_example/olfactometer_example.py +++ b/docs/examples/olfactometer_example/olfactometer_example.py @@ -2,11 +2,10 @@ import time from threading import Event, Thread -from serial import SerialException - -from harp.communication.device import Device, OperationMode from harp.protocol import MessageType, PayloadType from harp.protocol.messages import HarpMessage +from harp.serial.device import Device, OperationMode +from serial import SerialException SERIAL_PORT = ( "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port) diff --git a/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py b/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py index a43019f..7533fe1 100755 --- a/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py +++ b/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py @@ -1,8 +1,7 @@ -from serial import SerialException - -from harp.communication.device import Device from harp.protocol import MessageType, PayloadType from harp.protocol.messages import HarpMessage +from harp.serial.device import Device +from serial import SerialException SERIAL_PORT = ( "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port) diff --git a/docs/examples/wait_for_events/wait_for_events.py b/docs/examples/wait_for_events/wait_for_events.py index 7946464..e830e3d 100755 --- a/docs/examples/wait_for_events/wait_for_events.py +++ b/docs/examples/wait_for_events/wait_for_events.py @@ -1,5 +1,5 @@ -from harp.communication.device import Device from harp.protocol import OperationMode +from harp.serial.device import Device SERIAL_PORT = ( "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port) From ea50e70799c47f594f63a02f6879f0265fc81587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Grilo?= Date: Tue, 16 Sep 2025 10:31:08 +0100 Subject: [PATCH 150/267] Refactor: simplify WriteHarpMessage handling of PayloadType --- src/harp-protocol/harp/protocol/messages.py | 34 +++++++++++---------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/harp-protocol/harp/protocol/messages.py b/src/harp-protocol/harp/protocol/messages.py index 9b6e366..05b6311 100644 --- a/src/harp-protocol/harp/protocol/messages.py +++ b/src/harp-protocol/harp/protocol/messages.py @@ -1,7 +1,7 @@ from __future__ import annotations # for type hints (PEP 563) import struct -from typing import Union, Optional +from typing import Optional, Union from harp.protocol import MessageType, PayloadType @@ -516,22 +516,22 @@ class WriteHarpMessage(HarpMessage): # Define payload type properties _PAYLOAD_CONFIG = { # payload_type: (byte_size, signed, is_float) - PayloadType.U8: (1, False, False), - PayloadType.S8: (1, True, False), - PayloadType.U16: (2, False, False), - PayloadType.S16: (2, True, False), - PayloadType.U32: (4, False, False), - PayloadType.S32: (4, True, False), - PayloadType.U64: (8, False, False), - PayloadType.S64: (8, True, False), - PayloadType.Float: (4, False, True), + PayloadType.U8: (1, False), + PayloadType.S8: (1, True), + PayloadType.U16: (2, False), + PayloadType.S16: (2, True), + PayloadType.U32: (4, False), + PayloadType.S32: (4, True), + PayloadType.U64: (8, False), + PayloadType.S64: (8, True), + PayloadType.Float: (4, False), } def __init__( self, payload_type: PayloadType, address: int, - value: Optional[int | float | list[int] | list[float]] = None, + value: int | float | list[int] | list[float], ): """ Create a WriteHarpMessage to send to a device. @@ -554,16 +554,18 @@ def __init__( self._frame = bytearray() # Get configuration for this payload type - byte_size, signed, is_float = self._PAYLOAD_CONFIG.get( - payload_type, (1, False, False) - ) + byte_size, signed = self._PAYLOAD_CONFIG.get(payload_type, (1, False)) # Convert value to payload bytes payload = bytearray() - values = value if isinstance(value, list) else [value] + + if isinstance(value, int) or isinstance(value, float): + values = [value] + else: + values = value for val in values: - if is_float: + if isinstance(val, float): payload += struct.pack(" Date: Tue, 16 Sep 2025 10:32:15 +0100 Subject: [PATCH 151/267] Chore: add VSCode setting that enables pyrefly to display type errors --- .vscode/settings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index fcf64d1..3ad43fe 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,5 +10,6 @@ "tests" ], "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true + "python.testing.pytestEnabled": true, + "python.pyrefly.displayTypeErrors": "force-on", } \ No newline at end of file From 36ec9adb3ee97e5cecb56c197718b5fb642d1944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Tue, 16 Sep 2025 10:35:25 +0100 Subject: [PATCH 152/267] Removed custom message from exceptions --- src/harp-protocol/harp/protocol/exceptions.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/harp-protocol/harp/protocol/exceptions.py b/src/harp-protocol/harp/protocol/exceptions.py index 915b9ce..61650af 100644 --- a/src/harp-protocol/harp/protocol/exceptions.py +++ b/src/harp-protocol/harp/protocol/exceptions.py @@ -9,10 +9,9 @@ class HarpWriteException(HarpException): Exception raised when there is an error writing to a register in the Harp device. """ - def __init__(self, register, message): - super().__init__(f"Error writing to register {register}: {message}") + def __init__(self, register): + super().__init__(f"Error writing to register {register}") self.register = register - self.message = message class HarpReadException(HarpException): @@ -20,10 +19,9 @@ class HarpReadException(HarpException): Exception raised when there is an error reading from a register in the Harp device. """ - def __init__(self, register, message): - super().__init__(f"Error reading from register {register}: {message}") + def __init__(self, register): + super().__init__(f"Error reading from register {register}") self.register = register - self.message = message class HarpTimeoutError(HarpException): From 3272e1084182a380f9609e9a6481e1b9e02c10fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Tue, 16 Sep 2025 10:37:15 +0100 Subject: [PATCH 153/267] Pin harp-protocol version on harp-serial --- src/harp-serial/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/harp-serial/pyproject.toml b/src/harp-serial/pyproject.toml index 0432a75..cd4421b 100644 --- a/src/harp-serial/pyproject.toml +++ b/src/harp-serial/pyproject.toml @@ -7,7 +7,7 @@ license = "MIT" keywords = ['python', 'harp'] requires-python = ">=3.9,<4.0" dependencies = [ - "harp-protocol", + "harp-protocol==0.3.0", "pyserial>=3.5", ] From 34de54d8eb8dc56aaceb75df347ae1fa79273c3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Tue, 16 Sep 2025 10:52:35 +0100 Subject: [PATCH 154/267] Fix: raise HarpReadException for missing timestamp in ReplyHarpMessage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- src/harp-protocol/harp/protocol/messages.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/harp-protocol/harp/protocol/messages.py b/src/harp-protocol/harp/protocol/messages.py index 05b6311..75f77d0 100644 --- a/src/harp-protocol/harp/protocol/messages.py +++ b/src/harp-protocol/harp/protocol/messages.py @@ -4,6 +4,7 @@ from typing import Optional, Union from harp.protocol import MessageType, PayloadType +from harp.protocol.exceptions import HarpReadException class HarpMessage: @@ -443,7 +444,7 @@ def __init__( # Timestamp is junk if it's not present. if not (self.payload_type & PayloadType.Timestamp): - self._timestamp = None + raise HarpReadException(self.address) @property def is_error(self) -> bool: From a107ebb6c3a5c73082f3705c246ff82574797748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Tue, 16 Sep 2025 10:53:17 +0100 Subject: [PATCH 155/267] Add LaserDriverController to device_names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- src/harp-protocol/harp/protocol/device_names.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/harp-protocol/harp/protocol/device_names.py b/src/harp-protocol/harp/protocol/device_names.py index 3f31656..4682267 100644 --- a/src/harp-protocol/harp/protocol/device_names.py +++ b/src/harp-protocol/harp/protocol/device_names.py @@ -35,6 +35,7 @@ 1280: "SoundCard", 1282: "CurrentDriver", 1296: "SyringePump", + 1298: "LaserDriverController", 1400: "LicketySplit", 1401: "SniffDetector", 1402: "Treadmill", From ed7388aa0bb7702d2dcdc63fe1605ea19f9f27f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Tue, 16 Sep 2025 10:54:27 +0100 Subject: [PATCH 156/267] Refactor payload handling in HarpMessage to properly support Python 3.9 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- src/harp-protocol/harp/protocol/messages.py | 244 ++++++++++---------- 1 file changed, 122 insertions(+), 122 deletions(-) diff --git a/src/harp-protocol/harp/protocol/messages.py b/src/harp-protocol/harp/protocol/messages.py index 75f77d0..1ee5595 100644 --- a/src/harp-protocol/harp/protocol/messages.py +++ b/src/harp-protocol/harp/protocol/messages.py @@ -149,147 +149,147 @@ def payload(self) -> Union[int, list[int], bytearray, float, list[float]]: payload_index = payload_start + 1 # length is payload_start + payload type size - match self.payload_type: - case PayloadType.U8 | PayloadType.TimestampedU8: - if self.length == payload_start + 1: - return self._frame[payload_index] - else: # array case - return [ - int.from_bytes([self._frame[i]], byteorder="little") - for i in range(payload_index, self.length + 1) - ] - - case PayloadType.S8 | PayloadType.TimestampedS8: - if self.length == payload_start + 1: - return int.from_bytes( - [self._frame[payload_index]], byteorder="little", signed=True + pt = self.payload_type + if pt == PayloadType.U8 or pt == PayloadType.TimestampedU8: + if self.length == payload_start + 1: + return self._frame[payload_index] + else: # array case + return [ + int.from_bytes([self._frame[i]], byteorder="little") + for i in range(payload_index, self.length + 1) + ] + + elif pt == PayloadType.S8 or pt == PayloadType.TimestampedS8: + if self.length == payload_start + 1: + return int.from_bytes( + [self._frame[payload_index]], byteorder="little", signed=True + ) + else: # array case + return [ + int.from_bytes( + [self._frame[i]], + byteorder="little", + signed=True, ) - else: # array case - return [ - int.from_bytes( - [self._frame[i]], - byteorder="little", - signed=True, - ) - for i in range(payload_index, self.length + 1) - ] - - case PayloadType.U16 | PayloadType.TimestampedU16: - if self.length == payload_start + 2: - return int.from_bytes( - self._frame[payload_index : payload_index + 2], + for i in range(payload_index, self.length + 1) + ] + + elif pt == PayloadType.U16 or pt == PayloadType.TimestampedU16: + if self.length == payload_start + 2: + return int.from_bytes( + self._frame[payload_index : payload_index + 2], + byteorder="little", + signed=False, + ) + else: # array case + return [ + int.from_bytes( + self._frame[i : i + 2], byteorder="little", signed=False, ) - else: # array case - return [ - int.from_bytes( - self._frame[i : i + 2], - byteorder="little", - signed=False, - ) - for i in range(payload_index, self.length + 1, 2) - ] - - case PayloadType.S16 | PayloadType.TimestampedS16: - if self.length == payload_start + 2: - return int.from_bytes( - self._frame[payload_index : payload_index + 2], + for i in range(payload_index, self.length + 1, 2) + ] + + elif pt == PayloadType.S16 or pt == PayloadType.TimestampedS16: + if self.length == payload_start + 2: + return int.from_bytes( + self._frame[payload_index : payload_index + 2], + byteorder="little", + signed=True, + ) + else: + return [ + int.from_bytes( + self._frame[i : i + 2], byteorder="little", signed=True, ) - else: - return [ - int.from_bytes( - self._frame[i : i + 2], - byteorder="little", - signed=True, - ) - for i in range(payload_index, self.length + 1, 2) - ] - - case PayloadType.U32 | PayloadType.TimestampedU32: - if self.length == payload_start + 4: - return int.from_bytes( - self._frame[payload_index : payload_index + 4], + for i in range(payload_index, self.length + 1, 2) + ] + + elif pt == PayloadType.U32 or pt == PayloadType.TimestampedU32: + if self.length == payload_start + 4: + return int.from_bytes( + self._frame[payload_index : payload_index + 4], + byteorder="little", + signed=False, + ) + else: + return [ + int.from_bytes( + self._frame[i : i + 4], byteorder="little", signed=False, ) - else: - return [ - int.from_bytes( - self._frame[i : i + 4], - byteorder="little", - signed=False, - ) - for i in range(payload_index, self.length + 1, 4) - ] - - case PayloadType.S32 | PayloadType.TimestampedS32: - if self.length == payload_start + 4: - return int.from_bytes( - self._frame[payload_index : payload_index + 4], + for i in range(payload_index, self.length + 1, 4) + ] + + elif pt == PayloadType.S32 or pt == PayloadType.TimestampedS32: + if self.length == payload_start + 4: + return int.from_bytes( + self._frame[payload_index : payload_index + 4], + byteorder="little", + signed=True, + ) + else: + return [ + int.from_bytes( + self._frame[i : i + 4], byteorder="little", signed=True, ) - else: - return [ - int.from_bytes( - self._frame[i : i + 4], - byteorder="little", - signed=True, - ) - for i in range(payload_index, self.length + 1, 4) - ] - - case PayloadType.U64 | PayloadType.TimestampedU64: - if self.length == payload_start + 8: - return int.from_bytes( - self._frame[payload_index : payload_index + 8], + for i in range(payload_index, self.length + 1, 4) + ] + + elif pt == PayloadType.U64 or pt == PayloadType.TimestampedU64: + if self.length == payload_start + 8: + return int.from_bytes( + self._frame[payload_index : payload_index + 8], + byteorder="little", + signed=False, + ) + else: + return [ + int.from_bytes( + self._frame[i : i + 8], byteorder="little", signed=False, ) - else: - return [ - int.from_bytes( - self._frame[i : i + 8], - byteorder="little", - signed=False, - ) - for i in range(payload_index, self.length + 1, 8) - ] - - case PayloadType.S64 | PayloadType.TimestampedS64: - if self.length == payload_start + 8: - return int.from_bytes( - self._frame[payload_index : payload_index + 8], + for i in range(payload_index, self.length + 1, 8) + ] + + elif pt == PayloadType.S64 or pt == PayloadType.TimestampedS64: + if self.length == payload_start + 8: + return int.from_bytes( + self._frame[payload_index : payload_index + 8], + byteorder="little", + signed=True, + ) + else: + return [ + int.from_bytes( + self._frame[i : i + 8], byteorder="little", signed=True, ) - else: - return [ - int.from_bytes( - self._frame[i : i + 8], - byteorder="little", - signed=True, - ) - for i in range(payload_index, self.length + 1, 8) - ] - - case PayloadType.Float | PayloadType.TimestampedFloat: - if self.length == payload_start + 4: - return struct.unpack( - " int: From fbd7d64265bcde0e02654f254606e667f0d47a27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Thu, 18 Sep 2025 10:57:05 +0100 Subject: [PATCH 157/267] Add operation_ctrl methods to device MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- src/harp-serial/harp/serial/device.py | 105 +++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 2 deletions(-) diff --git a/src/harp-serial/harp/serial/device.py b/src/harp-serial/harp/serial/device.py index 5c4063d..107f313 100644 --- a/src/harp-serial/harp/serial/device.py +++ b/src/harp-serial/harp/serial/device.py @@ -7,6 +7,7 @@ from pathlib import Path from typing import Optional +import serial from harp.protocol import ( ClockConfig, CommonRegisters, @@ -21,8 +22,6 @@ from harp.protocol.messages import HarpMessage, ReplyHarpMessage from harp.serial.harp_serial import HarpSerial -import serial - class TimeoutStrategy(Enum): RAISE = "raise" # Raise HarpTimeoutError @@ -223,6 +222,108 @@ def dump_registers(self) -> list: break return replies + def read_operation_ctrl(self): + """ + Reads the OPERATION_CTRL register of the device. + + Returns + ------- + ReplyHarpMessage + The reply to the Harp message + """ + address = CommonRegisters.OPERATION_CTRL + reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8)) + + # create dict with complete byte and then decode each bit according to the OperationCtrl entries + if reply is not None: + reg_value = reply.payload + result = { + "REG_VALUE": reply.payload, + "OP_MODE": OperationMode(reg_value & OperationCtrl.OP_MODE), + "DUMP": bool(reg_value & OperationCtrl.DUMP), + "MUTE_RPL": bool(reg_value & OperationCtrl.MUTE_RPL), + "VISUALEN": bool(reg_value & OperationCtrl.VISUALEN), + "OPLEDEN": bool(reg_value & OperationCtrl.OPLEDEN), + "ALIVE_EN": bool(reg_value & OperationCtrl.ALIVE_EN), + } + return result + + def write_operation_ctrl( + self, + mode: Optional[OperationMode] = None, + mute_rpl: Optional[bool] = None, + visual_en: Optional[bool] = None, + op_led_en: Optional[bool] = None, + alive_en: Optional[bool] = None, + ) -> ReplyHarpMessage | None: + """ + Writes the OPERATION_CTRL register of the device. + + Parameters + ---------- + mode : OperationMode, optional + The new operation mode value + mute_rpl : bool, optional + If True, the Replies to all the Commands are muted + visual_en : bool, optional + If True, enables the status led + op_led_en : bool, optional + If True, enables the operation LED + alive_en : bool, optional + If True, enables the ALIVE_EN bit + Returns + ------- + ReplyHarpMessage + The reply to the Harp message + """ + address = CommonRegisters.OPERATION_CTRL + + # Read register first + reg_value = self.send( + HarpMessage.create(MessageType.READ, address, PayloadType.U8) + ) + + if reg_value is None: + return reg_value + + reg_value = reg_value.payload + + if mode is not None: + # Clear old operation mode + reg_value &= ~OperationCtrl.OP_MODE + # Set new operation mode + reg_value |= mode + + if mute_rpl is not None: + if mute_rpl: + reg_value |= OperationCtrl.MUTE_RPL + else: + reg_value &= ~OperationCtrl.MUTE_RPL + + if visual_en is not None: + if visual_en: + reg_value |= OperationCtrl.VISUALEN + else: + reg_value &= ~OperationCtrl.VISUALEN + + if op_led_en is not None: + if op_led_en: + reg_value |= OperationCtrl.OPLEDEN + else: + reg_value &= ~OperationCtrl.OPLEDEN + + if alive_en is not None: + if alive_en: + reg_value |= OperationCtrl.ALIVE_EN + else: + reg_value &= ~OperationCtrl.ALIVE_EN + + reply = self.send( + HarpMessage.create(MessageType.WRITE, address, PayloadType.U8, reg_value) + ) + + return reply + def set_mode(self, mode: OperationMode) -> ReplyHarpMessage | None: """ Sets the operation mode of the device. From af7e79f00158b91b2662ea86b5a5fb7b019fbddc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Thu, 18 Sep 2025 11:12:36 +0100 Subject: [PATCH 158/267] Update documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- README.md | 75 +++---------------------- docs/api/device.md | 1 - docs/api/messages.md | 4 -- docs/api/protocol.md | 8 +++ docs/api/serial.md | 6 ++ docs/index.md | 73 +----------------------- mkdocs.yml | 3 +- src/harp-protocol/README.md | 7 +++ src/harp-serial/README.md | 75 +++++++++++++++++++++++++ src/harp-serial/harp/serial/__init__.py | 1 + src/harp-serial/harp/serial/device.py | 15 +++++ 11 files changed, 121 insertions(+), 147 deletions(-) delete mode 100644 docs/api/device.md delete mode 100644 docs/api/messages.md create mode 100644 docs/api/serial.md create mode 100644 src/harp-protocol/README.md create mode 100644 src/harp-serial/README.md diff --git a/README.md b/README.md index 7bf37af..811f3d9 100644 --- a/README.md +++ b/README.md @@ -1,73 +1,12 @@ -# harp +# pyharp -Python implementation of the Harp protocol for hardware control and data acquisition. +This project includes two main packages: -## Installation + - **harp-protocol**: Provides the core protocol definitions and utilities for the Harp protocol. + See [Protocol API Documentation](https://fchampalimaud.github.io/pyharp/api/protocol) for details. -```bash -uv add harp-protocol -# or -pip install harp-protocol -``` + - **harp-serial**: Implements serial communication functionalities for generic Harp devices. + See [Serial API Documentation](https://fchampalimaud.github.io/pyharp/api/serial) for more information. -## Quick Start -```python -from harp import MessageType, PayloadType -from harp.device import Device -from harp.messages import HarpMessage - -# Connect to a device -device = Device("/dev/ttyUSB0") -#device = Device("COM3") # for Windows - -# Get device information -device.info() - -# define register_address -register_address = 32 - -# Read from register -value = device.send(HarpMessage.create(MessageType.READ, register_address, PayloadType.U8)) - -# Write to register -device.send(HarpMessage.create(MessageType.WRITE, register_address, PayloadType.U8, value)) - -# Disconnect when done -device.disconnect() -``` - -or using the `with` statement: - -```python -from harp import MessageType, PayloadType -from harp.device import Device -from harp.messages import HarpMessage - -with Device("/dev/ttyUSB0") as device: - # Get device information - device.info() - - # define register_address - register_address = 32 - - # Read from register - value = device.send(HarpMessage.create(MessageType.READ, register_address, PayloadType.U8)) - - # Write to register - device.send(HarpMessage.create(MessageType.WRITE, register_address, PayloadType.U8, value)) -``` - -## for Linux - -### Install UDEV Rules - -Install by either copying `10-harp.rules` over to your `/etc/udev/rules.d` folder or by symlinking it with: -```` -sudo ln -s /absolute/path/to/10-harp.rules /etc/udev/rules.d/10-harp.rules -```` - -Then reload udev rules with -```` -sudo udevadm control --reload-rules -```` +For specific Harp devices' packages please select the corresponding Harp device under the Devices section on the menu. \ No newline at end of file diff --git a/docs/api/device.md b/docs/api/device.md deleted file mode 100644 index a128d21..0000000 --- a/docs/api/device.md +++ /dev/null @@ -1 +0,0 @@ -::: harp.serial.Device diff --git a/docs/api/messages.md b/docs/api/messages.md deleted file mode 100644 index 0621fd0..0000000 --- a/docs/api/messages.md +++ /dev/null @@ -1,4 +0,0 @@ -::: harp.protocol.messages.HarpMessage -::: harp.protocol.messages.ReplyHarpMessage -::: harp.protocol.messages.ReadHarpMessage -::: harp.protocol.messages.WriteHarpMessage diff --git a/docs/api/protocol.md b/docs/api/protocol.md index 4e54672..f2e55ee 100644 --- a/docs/api/protocol.md +++ b/docs/api/protocol.md @@ -1,3 +1,7 @@ +{% include-markdown "../../src/harp-protocol/README.md" %} + +--- + ::: harp.protocol.MessageType ::: harp.protocol.PayloadType ::: harp.protocol.CommonRegisters @@ -5,3 +9,7 @@ ::: harp.protocol.OperationCtrl ::: harp.protocol.ResetMode ::: harp.protocol.ClockConfig +::: harp.protocol.messages.HarpMessage +::: harp.protocol.messages.ReplyHarpMessage +::: harp.protocol.messages.ReadHarpMessage +::: harp.protocol.messages.WriteHarpMessage diff --git a/docs/api/serial.md b/docs/api/serial.md new file mode 100644 index 0000000..c598ff0 --- /dev/null +++ b/docs/api/serial.md @@ -0,0 +1,6 @@ +{% include-markdown "../../src/harp-serial/README.md" %} + +--- + +::: harp.serial.Device +::: harp.serial.TimeoutStrategy diff --git a/docs/index.md b/docs/index.md index d50f918..96d83c6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,72 +1 @@ -# harp - -Python implementation of the Harp protocol for hardware control and data acquisition. - -## Installation - -```bash -uv add harp-protocol -# or -pip install harp-protocol -``` - -## Quick Start - -```python -from harp import MessageType, PayloadType -from harp.serial import Device -from harp.protocol.messages import HarpMessage - -# Connect to a device -device = Device("/dev/ttyUSB0") - -# Get device information -device.info() - -# define register_address -register_address = 32 - -# Read from register -value = device.send(HarpMessage.create(MessageType.READ, register_address, PayloadType.U8)) - -# Write to register -device.send(HarpMessage.create(MessageType.WRITE, register_address, PayloadType.U8, value.payload)) - -# Disconnect when done -device.disconnect() -``` - -or using the `with` statement: - -```python -from harp import MessageType, PayloadType -from harp.serial import Device -from harp.protocol.messages import HarpMessage - -with Device("/dev/ttyUSB0") as device: - # Get device information - device.info() - - # define register_address - register_address = 32 - - # Read from register - value = device.send(HarpMessage.create(MessageType.READ, register_address, PayloadType.U8)) - - # Write to register - device.send(HarpMessage.create(MessageType.WRITE, register_address, PayloadType.U8, value.payload)) -``` - -## for Linux - -### Install UDEV Rules - -Install by either copying `10-harp.rules` over to your `/etc/udev/rules.d` folder or by symlinking it with: -```` -sudo ln -s /absolute/path/to/10-harp.rules /etc/udev/rules.d/10-harp.rules -```` - -Then reload udev rules with -```` -sudo udevadm control --reload-rules -```` +{% include-markdown "../README.md" %} \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 1cd5215..32e9df3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -78,8 +78,7 @@ nav: - Home: index.md - API: - Protocol: api/protocol.md - - Device: api/device.md - - Messages: api/messages.md + - Serial: api/serial.md - Examples: - examples/index.md - Getting Device Info: examples/get_info/get_info.md diff --git a/src/harp-protocol/README.md b/src/harp-protocol/README.md new file mode 100644 index 0000000..80289eb --- /dev/null +++ b/src/harp-protocol/README.md @@ -0,0 +1,7 @@ +# harp-protocol + +[![PyPI version](https://badge.fury.io/py/harp-protocol.svg)](https://badge.fury.io/py/harp-protocol) + +The Harp Protocol is a binary communication protocol created in order to facilitate and unify the interaction between different devices. It was designed with efficiency and ease of parsing in mind. + +For more detail please check Harp Tech's official documentation [here](https://harp-tech.org/protocol/BinaryProtocol-8bit.html). diff --git a/src/harp-serial/README.md b/src/harp-serial/README.md new file mode 100644 index 0000000..d41a8f9 --- /dev/null +++ b/src/harp-serial/README.md @@ -0,0 +1,75 @@ +# harp-serial + +[![PyPI version](https://badge.fury.io/py/harp-serial.svg)](https://badge.fury.io/py/harp-serial) + +A Python library for communicating with Harp devices over serial connections. + +## Installation + +```bash +uv add harp-serial +# or +pip install harp-serial +``` + +## Quick Start + +```python +from harp.protocol import MessageType, PayloadType +from harp.protocol.messages import HarpMessage +from harp.serial.device import Device + +# Connect to a device +device = Device("/dev/ttyUSB0") +#device = Device("COM3") # for Windows + +# Get device information +device.info() + +# define register_address +register_address = 32 + +# Read from register +reply = device.send(HarpMessage.create(MessageType.READ, register_address, PayloadType.U8)) + +# Write to register +device.send(HarpMessage.create(MessageType.WRITE, register_address, PayloadType.U8, reply.payload)) + +# Disconnect when done +device.disconnect() +``` + +or using the `with` statement: + +```python +from harp.protocol import MessageType, PayloadType +from harp.protocol.messages import HarpMessage +from harp.serial.device import Device + +with Device("/dev/ttyUSB0") as device: + # Get device information + device.info() + + # define register_address + register_address = 32 + + # Read from register + reply = device.send(HarpMessage.create(MessageType.READ, register_address, PayloadType.U8)) + + # Write to register + device.send(HarpMessage.create(MessageType.WRITE, register_address, PayloadType.U8, reply.payload)) +``` + +## for Linux + +### Install UDEV Rules + +Install by either copying `10-harp.rules` over to your `/etc/udev/rules.d` folder or by symlinking it with: +```` +sudo ln -s /absolute/path/to/10-harp.rules /etc/udev/rules.d/10-harp.rules +```` + +Then reload udev rules with +```` +sudo udevadm control --reload-rules +```` diff --git a/src/harp-serial/harp/serial/__init__.py b/src/harp-serial/harp/serial/__init__.py index 7af9dc5..811fc78 100644 --- a/src/harp-serial/harp/serial/__init__.py +++ b/src/harp-serial/harp/serial/__init__.py @@ -1,2 +1,3 @@ from .device import Device as Device +from .device import TimeoutStrategy as TimeoutStrategy from .harp_serial import HarpSerial as HarpSerial diff --git a/src/harp-serial/harp/serial/device.py b/src/harp-serial/harp/serial/device.py index 107f313..c680687 100644 --- a/src/harp-serial/harp/serial/device.py +++ b/src/harp-serial/harp/serial/device.py @@ -24,6 +24,21 @@ class TimeoutStrategy(Enum): + """ + Strategy to handle timeouts when waiting for a reply from the device. + + Attributes + ---------- + RAISE : str + Raise HarpTimeoutError + RETURN_NONE : str + Return None + LOG_AND_RAISE : str + Log the timeout and raise HarpTimeoutError + LOG_AND_NONE : str + Log the timeout and return None + """ + RAISE = "raise" # Raise HarpTimeoutError RETURN_NONE = "return_none" # Return None LOG_AND_RAISE = "log_and_raise" From 5ea52bbbacd5119010c725c9e90755fe3fbebee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Thu, 18 Sep 2025 11:21:16 +0100 Subject: [PATCH 159/267] Fix tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- tests/test_messages.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/tests/test_messages.py b/tests/test_messages.py index 45ab698..6b2fb18 100644 --- a/tests/test_messages.py +++ b/tests/test_messages.py @@ -1,6 +1,7 @@ import pytest from harp.protocol import CommonRegisters, MessageType, PayloadType +from harp.protocol.exceptions import HarpReadException from harp.protocol.messages import ( HarpMessage, ReadHarpMessage, @@ -57,7 +58,7 @@ def test_reply_is_error(): 5, 42, 255, - PayloadType.U8, + PayloadType.TimestampedU8, 0, 0, 0, @@ -83,7 +84,7 @@ def test_reply_is_error(): 5, 42, 255, - PayloadType.U8, + PayloadType.TimestampedU8, 0, 0, 0, @@ -385,7 +386,7 @@ def test_reply_message_str_repr() -> None: 5, 42, 255, - PayloadType.U8, + PayloadType.TimestampedU8, 0, 0, 0, @@ -424,7 +425,7 @@ def test_payload_as_string() -> None: 5 + len(encoded), 42, 255, - PayloadType.U8, + PayloadType.TimestampedU8, 0, 0, 0, @@ -507,20 +508,17 @@ def test_timestamp_handling() -> None: assert reply.timestamp is not None assert reply.timestamp == 1 + 32 * 32e-6 # 1 second + 1 millisecond - # Create a non-timestamped message + +# test ReplyHarpMessage without TimestampedU8 raises HarpReadException +def test_reply_without_timestamp_raises() -> None: + """Test that accessing timestamp in non-timestamped message raises exception.""" frame = bytearray( [ MessageType.READ, 5, 42, 255, - PayloadType.U8, - 1, - 0, - 0, - 0, # timestamp seconds = 1 - 32, - 0, # timestamp micros = 32 (= 1ms) + PayloadType.U8, # Not a timestamped type 123, # payload 0, ] @@ -530,8 +528,9 @@ def test_timestamp_handling() -> None: checksum = sum(frame[:-1]) & 255 frame[-1] = checksum - reply = ReplyHarpMessage(frame) - assert reply.timestamp is None + with pytest.raises(HarpReadException) as excinfo: + ReplyHarpMessage(frame) + assert "not a timestamped payload type" in str(excinfo.value) def test_calculate_checksum() -> None: From d7a6a632cb4d0e4d2b9e70d29f55cda4c2c104bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 8 Oct 2025 15:50:22 +0100 Subject: [PATCH 160/267] Remove unnecessary udev rules file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- 10-harp.rules | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 10-harp.rules diff --git a/10-harp.rules b/10-harp.rules deleted file mode 100644 index c5114ef..0000000 --- a/10-harp.rules +++ /dev/null @@ -1,3 +0,0 @@ -# UDEV rules for a Harp Device (actually an ftdi RS232 Serial [Uart] IC) -SUBSYSTEMS=="usb", ENV{.LOCAL_ifNum}="$attr{bInterfaceNumber}" -SUBSYSTEMS=="usb", KERNEL=="ttyUSB*", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", MODE="0666", SYMLINK+="harp_device_%E{.LOCAL_ifNum}" From d454897c6ae51d9ea1e22a748e663e9ba74304e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 8 Oct 2025 15:54:23 +0100 Subject: [PATCH 161/267] Refactor HARP_VERSION to CORE_VERSION MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- src/harp-protocol/harp/protocol/base.py | 12 ++++++------ src/harp-serial/harp/serial/device.py | 26 ++++++++++++------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/harp-protocol/harp/protocol/base.py b/src/harp-protocol/harp/protocol/base.py index 8282c93..17f16e4 100644 --- a/src/harp-protocol/harp/protocol/base.py +++ b/src/harp-protocol/harp/protocol/base.py @@ -117,10 +117,10 @@ class CommonRegisters(IntEnum): The number of the `HW_VERSION_L` register ASSEMBLY_VERSION : int The number of the `ASSEMBLY_VERSION` register - HARP_VERSION_H : int - The number of the `HARP_VERSION_H` register - HARP_VERSION_L : int - The number of the `HARP_VERSION_L` register + CORE_VERSION_H : int + The number of the `CORE_VERSION_H` register + CORE_VERSION_L : int + The number of the `CORE_VERSION_L` register FIRMWARE_VERSION_H : int The number of the `FIRMWARE_VERSION_H` register FIRMWARE_VERSION_L : int @@ -147,8 +147,8 @@ class CommonRegisters(IntEnum): HW_VERSION_H = 0x01 HW_VERSION_L = 0x02 ASSEMBLY_VERSION = 0x03 - HARP_VERSION_H = 0x04 - HARP_VERSION_L = 0x05 + CORE_VERSION_H = 0x04 + CORE_VERSION_L = 0x05 FIRMWARE_VERSION_H = 0x06 FIRMWARE_VERSION_L = 0x07 TIMESTAMP_SECOND = 0x08 diff --git a/src/harp-serial/harp/serial/device.py b/src/harp-serial/harp/serial/device.py index c680687..75d8f35 100644 --- a/src/harp-serial/harp/serial/device.py +++ b/src/harp-serial/harp/serial/device.py @@ -80,8 +80,8 @@ class Device: HW_VERSION_H: int HW_VERSION_L: int ASSEMBLY_VERSION: int - HARP_VERSION_H: int - HARP_VERSION_L: int + CORE_VERSION_H: int + CORE_VERSION_L: int FIRMWARE_VERSION_H: int FIRMWARE_VERSION_L: int DEVICE_NAME: str @@ -134,8 +134,8 @@ def load(self) -> None: self.HW_VERSION_H = self._read_hw_version_h() self.HW_VERSION_L = self._read_hw_version_l() self.ASSEMBLY_VERSION = self._read_assembly_version() - self.HARP_VERSION_H = self._read_harp_version_h() - self.HARP_VERSION_L = self._read_harp_version_l() + self.CORE_VERSION_H = self._read_core_version_h() + self.CORE_VERSION_L = self._read_core_version_l() self.FIRMWARE_VERSION_H = self._read_fw_version_h() self.FIRMWARE_VERSION_L = self._read_fw_version_l() self.DEVICE_NAME = self._read_device_name() @@ -151,7 +151,7 @@ def info(self) -> None: print(f"* Who am I: ({self.WHO_AM_I}) {self.DEFAULT_DEVICE_NAME}") print(f"* HW version: {self.HW_VERSION_H}.{self.HW_VERSION_L}") print(f"* Assembly version: {self.ASSEMBLY_VERSION}") - print(f"* HARP version: {self.HARP_VERSION_H}.{self.HARP_VERSION_L}") + print(f"* HARP version: {self.CORE_VERSION_H}.{self.CORE_VERSION_L}") print( f"* Firmware version: {self.FIRMWARE_VERSION_H}.{self.FIRMWARE_VERSION_L}" ) @@ -1331,31 +1331,31 @@ def _read_assembly_version(self) -> int: return reply.payload - def _read_harp_version_h(self) -> int: + def _read_core_version_h(self) -> int: """ - Reads the value stored in the `HARP_VERSION_H` register. + Reads the value stored in the `CORE_VERSION_H` register. Returns ------- int - The value of the `HARP_VERSION_H` register + The value of the `CORE_VERSION_H` register """ - address = CommonRegisters.HARP_VERSION_H + address = CommonRegisters.CORE_VERSION_H reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8)) return reply.payload - def _read_harp_version_l(self) -> int: + def _read_core_version_l(self) -> int: """ - Reads the value stored in the `HARP_VERSION_L` register. + Reads the value stored in the `CORE_VERSION_L` register. Returns ------- int - The value of the `HARP_VERSION_L` register + The value of the `CORE_VERSION_L` register """ - address = CommonRegisters.HARP_VERSION_L + address = CommonRegisters.CORE_VERSION_L reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8)) From 4162bff92eaaa9710a79b931e87683b755e2369c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 8 Oct 2025 16:02:54 +0100 Subject: [PATCH 162/267] Update documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- src/harp-protocol/harp/protocol/base.py | 36 ++++++++++++------------- src/harp-serial/harp/serial/device.py | 4 ++- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/harp-protocol/harp/protocol/base.py b/src/harp-protocol/harp/protocol/base.py index 17f16e4..c22e3fd 100644 --- a/src/harp-protocol/harp/protocol/base.py +++ b/src/harp-protocol/harp/protocol/base.py @@ -190,21 +190,21 @@ class OperationCtrl(IntFlag): Attributes ---------- OP_MODE : int - Bits 1:0 (0x03): Operation mode of the device. + Operation mode of the device. 0: Standby Mode (all Events off, mandatory) 1: Active Mode (Events detection enabled, mandatory) 2: Reserved 3: Speed Mode (device enters Speed Mode, optional; only responds to Speed Mode commands) DUMP : int - Bit 3 (0x08): When set to 1, the device adds the content of all registers to the streaming buffer as Read messages. Always read as 0 + When set to 1, the device adds the content of all registers to the streaming buffer as Read messages. Always read as 0 MUTE_RPL : int - Bit 4 (0x10): If set to 1, replies to all commands are muted (not sent by the device) + If set to 1, replies to all commands are muted (not sent by the device) VISUALEN : int - Bit 5 (0x20): If set to 1, visual indications (e.g., LEDs) operate. If 0, all visual indications are turned off + If set to 1, visual indications (e.g., LEDs) operate. If 0, all visual indications are turned off OPLEDEN : int - Bit 6 (0x40): If set to 1, the LED indicates the selected Operation Mode (see LED feedback table in documentation) + If set to 1, the LED indicates the selected Operation Mode (see LED feedback table in documentation) ALIVE_EN : int - Bit 7 (0x80): If set to 1, the device sends an Event Message with the R_TIMESTAMP_SECONDS content each second (heartbeat) + If set to 1, the device sends an Event Message with the R_TIMESTAMP_SECONDS content each second (heartbeat) """ OP_MODE = 3 << 0 @@ -223,20 +223,20 @@ class ResetMode(IntEnum): Attributes ---------- RST_DEF : int - Bit 0 (0x01): If set, resets the device and restores all registers (Common and Application) to default values. + If set, resets the device and restores all registers (Common and Application) to default values. EEPROM is erased and defaults become the permanent boot option RST_EE : int - Bit 1 (0x02): If set, resets the device and restores all registers (Common and Application) from non-volatile memory (EEPROM). + If set, resets the device and restores all registers (Common and Application) from non-volatile memory (EEPROM). EEPROM values remain the permanent boot option SAVE : int - Bit 3 (0x08): If set, saves all non-volatile registers (Common and Application) to EEPROM and reboots. + If set, saves all non-volatile registers (Common and Application) to EEPROM and reboots. EEPROM becomes the permanent boot option NAME_TO_DEFAULT : int - Bit 4 (0x10): If set, reboots the device with the default name + If set, reboots the device with the default name BOOT_DEF : int - Bit 6 (0x40, read-only): Indicates the device booted with default register values + If set, indicates the device booted with default register values BOOT_EE : int - Bit 7 (0x80, read-only): Indicates the device booted with register values saved on the EEPROM + If set, indicates the device booted with register values saved on the EEPROM """ RST_DEF = 0x01 @@ -255,20 +255,20 @@ class ClockConfig(IntFlag): Attributes ---------- CLK_REP : int - Bit 0 (0x01): If set to 1, the device will repeat the Harp Synchronization Clock to the Clock Output connector, if available. + If set to 1, the device will repeat the Harp Synchronization Clock to the Clock Output connector, if available. Acts as a daisy-chain by repeating the Clock Input to the Clock Output. Setting this bit also unlocks the Harp Synchronization Clock CLK_GEN : int - Bit 1 (0x02): If set to 1, the device will generate Harp Synchronization Clock to the Clock Output connector, if available. + If set to 1, the device will generate Harp Synchronization Clock to the Clock Output connector, if available. The Clock Input will be ignored. Read as 1 if the device is generating the Harp Synchronization Clock REP_ABLE : int - Bit 3 (0x08, read-only): Indicates if the device is able (1) to repeat the Harp Synchronization Clock timestamp + If set, indicates if the device is able (1) to repeat the Harp Synchronization Clock timestamp GEN_ABLE : int - Bit 4 (0x10, read-only): Indicates if the device is able (1) to generate the Harp Synchronization Clock timestamp + If set, indicates if the device is able (1) to generate the Harp Synchronization Clock timestamp CLK_UNLOCK : int - Bit 6 (0x40): If set to 1, the device will unlock the timestamp register counter (R_TIMESTAMP_SECOND) and accept new timestamp values. + If set to 1, the device will unlock the timestamp register counter (R_TIMESTAMP_SECOND) and accept new timestamp values. Read as 1 if the timestamp register is unlocked CLK_LOCK : int - Bit 7 (0x80): If set to 1, the device will lock the current timestamp register counter (R_TIMESTAMP_SECOND) and reject new timestamp values. + If set to 1, the device will lock the current timestamp register counter (R_TIMESTAMP_SECOND) and reject new timestamp values. Read as 1 if the timestamp register is locked """ diff --git a/src/harp-serial/harp/serial/device.py b/src/harp-serial/harp/serial/device.py index 75d8f35..77468b4 100644 --- a/src/harp-serial/harp/serial/device.py +++ b/src/harp-serial/harp/serial/device.py @@ -111,7 +111,9 @@ def __init__( dump_file_path: str, optional The binary file to which all Harp messages will be written read_timeout_s: float, optional - _TODO_ + The timeout in seconds when waiting for a reply from the device + timeout_strategy: TimeoutStrategy, optional + The strategy to handle timeouts when waiting for a reply from the device """ self.log = logging.getLogger(f"{__name__}.{self.__class__.__name__}") self._serial_port = serial_port From 6e714ecf6704d3e907bae0267ba290f446775765 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 8 Oct 2025 17:02:21 +0100 Subject: [PATCH 163/267] Refactor base.py enums for consistency and clarity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- src/harp-protocol/harp/protocol/base.py | 145 +++++++++++++------- src/harp-protocol/harp/protocol/messages.py | 26 ++-- tests/test_messages.py | 16 +-- 3 files changed, 115 insertions(+), 72 deletions(-) diff --git a/src/harp-protocol/harp/protocol/base.py b/src/harp-protocol/harp/protocol/base.py index c22e3fd..88b5b88 100644 --- a/src/harp-protocol/harp/protocol/base.py +++ b/src/harp-protocol/harp/protocol/base.py @@ -4,12 +4,6 @@ # The reference epoch for UTC harp time REFERENCE_EPOCH = datetime(1904, 1, 1) -# Bit masks for the PayloadType -_isUnsigned: int = 0x00 -_isSigned: int = 0x80 -_isFloat: int = 0x40 -_hasTimestamp: int = 0x10 - class MessageType(IntEnum): """ @@ -29,11 +23,37 @@ class MessageType(IntEnum): The value that corresponds to a Write Error Harp message (10). Messages of this type are only meant to be send by the device """ - READ = 1 - WRITE = 2 - EVENT = 3 - READ_ERROR = 9 - WRITE_ERROR = 10 + READ = 0x01 + WRITE = 0x02 + EVENT = 0x03 + ERROR = 0x08 + READ_ERROR = READ | ERROR + WRITE_ERROR = WRITE | ERROR + + def is_error(self): + return bool(self & MessageType.ERROR) + + +class _PayloadTypeFlags(IntEnum): + """ + Internal flags used to define the PayloadType enumeration. + + Attributes + ---------- + HAS_TIMESTAMP : int + Flag indicating that the message has a timestamp + IS_FLOAT : int + Flag indicating that the message payload is a float + IS_SIGNED : int + Flag indicating that the message payload is signed + TYPE_SIZE : int + Mask to get the size of the message payload in bytes + """ + + HAS_TIMESTAMP = 0x10 + IS_FLOAT = 0x40 + IS_SIGNED = 0x80 + TYPE_SIZE = 0x0F class PayloadType(IntEnum): @@ -58,49 +78,72 @@ class PayloadType(IntEnum): The value that corresponds to a message of type U64 S64 : int The value that corresponds to a message of type S64 - Float : int + FLOAT : int The value that corresponds to a message of type Float - Timestamp: int + TIMESTAMP : int The value that corresponds to a message of type Timestamp. This is not a valid PayloadType, but it is used to indicate that the message has a timestamp. - TimestampedU8 : int + TIMESTAMPED_U8 : int The value that corresponds to a message of type TimestampedU8 - TimestampedS8 : int + TIMESTAMPED_S8 : int The value that corresponds to a message of type TimestampedS8 - TimestampedU16 : int + TIMESTAMPED_U16 : int The value that corresponds to a message of type TimestampedU16 - TimestampedS16 : int + TIMESTAMPED_S16 : int The value that corresponds to a message of type TimestampedS16 - TimestampedU32 : int + TIMESTAMPED_U32 : int The value that corresponds to a message of type TimestampedU32 - TimestampedS32 : int + TIMESTAMPED_S32 : int The value that corresponds to a message of type TimestampedS32 - TimestampedU64 : int + TIMESTAMPED_U64 : int The value that corresponds to a message of type TimestampedU64 - TimestampedS64 : int + TIMESTAMPED_S64 : int The value that corresponds to a message of type TimestampedS64 - TimestampedFloat : int + TIMESTAMPED_FLOAT : int The value that corresponds to a message of type TimestampedFloat """ - U8 = _isUnsigned | 1 - S8 = _isSigned | 1 - U16 = _isUnsigned | 2 - S16 = _isSigned | 2 - U32 = _isUnsigned | 4 - S32 = _isSigned | 4 - U64 = _isUnsigned | 8 - S64 = _isSigned | 8 - Float = _isFloat | 4 - Timestamp = _hasTimestamp - TimestampedU8 = _hasTimestamp | U8 - TimestampedS8 = _hasTimestamp | S8 - TimestampedU16 = _hasTimestamp | U16 - TimestampedS16 = _hasTimestamp | S16 - TimestampedU32 = _hasTimestamp | U32 - TimestampedS32 = _hasTimestamp | S32 - TimestampedU64 = _hasTimestamp | U64 - TimestampedS64 = _hasTimestamp | S64 - TimestampedFloat = _hasTimestamp | Float + U8 = 0x01 + S8 = _PayloadTypeFlags.IS_SIGNED | 0x01 + U16 = 0x02 + S16 = _PayloadTypeFlags.IS_SIGNED | 0x02 + U32 = 0x04 + S32 = _PayloadTypeFlags.IS_SIGNED | 0x04 + U64 = 0x08 + S64 = _PayloadTypeFlags.IS_SIGNED | 0x08 + FLOAT = _PayloadTypeFlags.IS_FLOAT | 0x04 + TIMESTAMPED_U8 = _PayloadTypeFlags.HAS_TIMESTAMP | U8 + TIMESTAMPED_S8 = _PayloadTypeFlags.HAS_TIMESTAMP | S8 + TIMESTAMPED_U16 = _PayloadTypeFlags.HAS_TIMESTAMP | U16 + TIMESTAMPED_S16 = _PayloadTypeFlags.HAS_TIMESTAMP | S16 + TIMESTAMPED_U32 = _PayloadTypeFlags.HAS_TIMESTAMP | U32 + TIMESTAMPED_S32 = _PayloadTypeFlags.HAS_TIMESTAMP | S32 + TIMESTAMPED_U64 = _PayloadTypeFlags.HAS_TIMESTAMP | U64 + TIMESTAMPED_S64 = _PayloadTypeFlags.HAS_TIMESTAMP | S64 + TIMESTAMPED_FLOAT = _PayloadTypeFlags.HAS_TIMESTAMP | FLOAT + + def has_timestamp(self): + """ + bool + Returns True if this PayloadType has a timestamp, False otherwise. + """ + return bool(self & _PayloadTypeFlags.HAS_TIMESTAMP) + + def is_float(self): + """ + bool + Returns True if this PayloadType is a float, False otherwise. + """ + return bool(self & _PayloadTypeFlags.IS_FLOAT) + + def is_signed(self): + """ + bool + Returns True if this PayloadType is signed, False otherwise. + """ + return bool(self & _PayloadTypeFlags.IS_SIGNED) + + def type_size(self): + return self & _PayloadTypeFlags.TYPE_SIZE class CommonRegisters(IntEnum): @@ -177,10 +220,10 @@ class OperationMode(IntEnum): The value that corresponds to the Speed operation mode (3). The device enters Speed Mode """ - STANDBY = 0 - ACTIVE = 1 - RESERVED = 2 - SPEED = 3 + STANDBY = 0x00 + ACTIVE = 0x01 + RESERVED = 0x02 + SPEED = 0x03 class OperationCtrl(IntFlag): @@ -207,12 +250,12 @@ class OperationCtrl(IntFlag): If set to 1, the device sends an Event Message with the R_TIMESTAMP_SECONDS content each second (heartbeat) """ - OP_MODE = 3 << 0 - DUMP = 1 << 3 - MUTE_RPL = 1 << 4 - VISUALEN = 1 << 5 - OPLEDEN = 1 << 6 - ALIVE_EN = 1 << 7 + OP_MODE = 0x03 + DUMP = 0x08 + MUTE_RPL = 0x10 + VISUALEN = 0x20 + OPLEDEN = 0x40 + ALIVE_EN = 0x80 class ResetMode(IntEnum): diff --git a/src/harp-protocol/harp/protocol/messages.py b/src/harp-protocol/harp/protocol/messages.py index 1ee5595..3715305 100644 --- a/src/harp-protocol/harp/protocol/messages.py +++ b/src/harp-protocol/harp/protocol/messages.py @@ -143,14 +143,14 @@ def payload(self) -> Union[int, list[int], bytearray, float, list[float]]: The payload sent in the write Harp message """ payload_start = self.BASE_LENGTH - if self.payload_type & PayloadType.Timestamp: + if self.payload_type.has_timestamp(): payload_start += 6 payload_index = payload_start + 1 # length is payload_start + payload type size pt = self.payload_type - if pt == PayloadType.U8 or pt == PayloadType.TimestampedU8: + if pt == PayloadType.U8 or pt == PayloadType.TIMESTAMPED_U8: if self.length == payload_start + 1: return self._frame[payload_index] else: # array case @@ -159,7 +159,7 @@ def payload(self) -> Union[int, list[int], bytearray, float, list[float]]: for i in range(payload_index, self.length + 1) ] - elif pt == PayloadType.S8 or pt == PayloadType.TimestampedS8: + elif pt == PayloadType.S8 or pt == PayloadType.TIMESTAMPED_S8: if self.length == payload_start + 1: return int.from_bytes( [self._frame[payload_index]], byteorder="little", signed=True @@ -174,7 +174,7 @@ def payload(self) -> Union[int, list[int], bytearray, float, list[float]]: for i in range(payload_index, self.length + 1) ] - elif pt == PayloadType.U16 or pt == PayloadType.TimestampedU16: + elif pt == PayloadType.U16 or pt == PayloadType.TIMESTAMPED_U16: if self.length == payload_start + 2: return int.from_bytes( self._frame[payload_index : payload_index + 2], @@ -191,7 +191,7 @@ def payload(self) -> Union[int, list[int], bytearray, float, list[float]]: for i in range(payload_index, self.length + 1, 2) ] - elif pt == PayloadType.S16 or pt == PayloadType.TimestampedS16: + elif pt == PayloadType.S16 or pt == PayloadType.TIMESTAMPED_S16: if self.length == payload_start + 2: return int.from_bytes( self._frame[payload_index : payload_index + 2], @@ -208,7 +208,7 @@ def payload(self) -> Union[int, list[int], bytearray, float, list[float]]: for i in range(payload_index, self.length + 1, 2) ] - elif pt == PayloadType.U32 or pt == PayloadType.TimestampedU32: + elif pt == PayloadType.U32 or pt == PayloadType.TIMESTAMPED_U32: if self.length == payload_start + 4: return int.from_bytes( self._frame[payload_index : payload_index + 4], @@ -225,7 +225,7 @@ def payload(self) -> Union[int, list[int], bytearray, float, list[float]]: for i in range(payload_index, self.length + 1, 4) ] - elif pt == PayloadType.S32 or pt == PayloadType.TimestampedS32: + elif pt == PayloadType.S32 or pt == PayloadType.TIMESTAMPED_S32: if self.length == payload_start + 4: return int.from_bytes( self._frame[payload_index : payload_index + 4], @@ -242,7 +242,7 @@ def payload(self) -> Union[int, list[int], bytearray, float, list[float]]: for i in range(payload_index, self.length + 1, 4) ] - elif pt == PayloadType.U64 or pt == PayloadType.TimestampedU64: + elif pt == PayloadType.U64 or pt == PayloadType.TIMESTAMPED_U64: if self.length == payload_start + 8: return int.from_bytes( self._frame[payload_index : payload_index + 8], @@ -259,7 +259,7 @@ def payload(self) -> Union[int, list[int], bytearray, float, list[float]]: for i in range(payload_index, self.length + 1, 8) ] - elif pt == PayloadType.S64 or pt == PayloadType.TimestampedS64: + elif pt == PayloadType.S64 or pt == PayloadType.TIMESTAMPED_S64: if self.length == payload_start + 8: return int.from_bytes( self._frame[payload_index : payload_index + 8], @@ -276,7 +276,7 @@ def payload(self) -> Union[int, list[int], bytearray, float, list[float]]: for i in range(payload_index, self.length + 1, 8) ] - elif pt == PayloadType.Float or pt == PayloadType.TimestampedFloat: + elif pt == PayloadType.FLOAT or pt == PayloadType.TIMESTAMPED_FLOAT: if self.length == payload_start + 4: return struct.unpack( " str: """ payload_str = "" format_str = "" - if self.payload_type in [PayloadType.Float, PayloadType.TimestampedFloat]: + if self.payload_type in [PayloadType.FLOAT, PayloadType.TIMESTAMPED_FLOAT]: format_str = ".6f" else: bytes_per_word = self.payload_type & 0x07 @@ -443,7 +443,7 @@ def __init__( ) # Timestamp is junk if it's not present. - if not (self.payload_type & PayloadType.Timestamp): + if not self.payload_type.has_timestamp(): raise HarpReadException(self.address) @property @@ -456,7 +456,7 @@ def is_error(self) -> bool: bool Returns True if this HarpMessage is an error message, False otherwise. """ - return self.message_type in [MessageType.READ_ERROR, MessageType.WRITE_ERROR] + return self.message_type.is_error() @property def timestamp(self) -> float: diff --git a/tests/test_messages.py b/tests/test_messages.py index 6b2fb18..35d85fe 100644 --- a/tests/test_messages.py +++ b/tests/test_messages.py @@ -58,7 +58,7 @@ def test_reply_is_error(): 5, 42, 255, - PayloadType.TimestampedU8, + PayloadType.TIMESTAMPED_U8, 0, 0, 0, @@ -84,7 +84,7 @@ def test_reply_is_error(): 5, 42, 255, - PayloadType.TimestampedU8, + PayloadType.TIMESTAMPED_U8, 0, 0, 0, @@ -161,7 +161,7 @@ def test_create_read_S64() -> None: def test_create_read_float() -> None: - message = ReadHarpMessage(payload_type=PayloadType.Float, address=DEFAULT_ADDRESS) + message = ReadHarpMessage(payload_type=PayloadType.FLOAT, address=DEFAULT_ADDRESS) assert message.message_type == MessageType.READ assert message.checksum == 114 # 1 + 4 + 42 + 255 + 4 - 256 @@ -304,7 +304,7 @@ def test_create_write_S64_array() -> None: def test_create_write_float_array() -> None: """Test creating a write message with float array values.""" values = [1.1, 2.2, 3.3] - message = WriteHarpMessage(PayloadType.Float, DEFAULT_ADDRESS, values) + message = WriteHarpMessage(PayloadType.FLOAT, DEFAULT_ADDRESS, values) assert message.message_type == MessageType.WRITE expected_checksum = 193 # (2 + 4 + 42 + 255 + 1 + 3 * 4) & 255 @@ -386,7 +386,7 @@ def test_reply_message_str_repr() -> None: 5, 42, 255, - PayloadType.TimestampedU8, + PayloadType.TIMESTAMPED_U8, 0, 0, 0, @@ -425,7 +425,7 @@ def test_payload_as_string() -> None: 5 + len(encoded), 42, 255, - PayloadType.TimestampedU8, + PayloadType.TIMESTAMPED_U8, 0, 0, 0, @@ -456,7 +456,7 @@ def test_harp_message_parse() -> None: 11, 42, 255, - PayloadType.TimestampedU8, + PayloadType.TIMESTAMPED_U8, 0, 0, 0, @@ -488,7 +488,7 @@ def test_timestamp_handling() -> None: 5, 42, 255, - PayloadType.TimestampedU8, + PayloadType.TIMESTAMPED_U8, 1, 0, 0, From 9a440b2ec1f9706dae4e3689501d3913bb8d5e0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Thu, 9 Oct 2025 09:31:59 +0100 Subject: [PATCH 164/267] Refactor HarpMessage to consolidate message creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- docs/api/protocol.md | 2 - src/harp-protocol/harp/protocol/messages.py | 156 +++++++------------- tests/test_messages.py | 77 +++++----- 3 files changed, 89 insertions(+), 146 deletions(-) diff --git a/docs/api/protocol.md b/docs/api/protocol.md index f2e55ee..2d765f6 100644 --- a/docs/api/protocol.md +++ b/docs/api/protocol.md @@ -11,5 +11,3 @@ ::: harp.protocol.ClockConfig ::: harp.protocol.messages.HarpMessage ::: harp.protocol.messages.ReplyHarpMessage -::: harp.protocol.messages.ReadHarpMessage -::: harp.protocol.messages.WriteHarpMessage diff --git a/src/harp-protocol/harp/protocol/messages.py b/src/harp-protocol/harp/protocol/messages.py index 3715305..4d95a74 100644 --- a/src/harp-protocol/harp/protocol/messages.py +++ b/src/harp-protocol/harp/protocol/messages.py @@ -4,7 +4,7 @@ from typing import Optional, Union from harp.protocol import MessageType, PayloadType -from harp.protocol.exceptions import HarpReadException +from harp.protocol.exceptions import HarpException, HarpReadException class HarpMessage: @@ -34,6 +34,60 @@ class HarpMessage: _frame: bytearray = bytearray() _port: int = DEFAULT_PORT + def __init__( + self, + message_type: MessageType, + payload_type: PayloadType, + address: int, + value: Optional[int | float | list[int] | list[float]] = None, + ): + """ + Parameters + ---------- + message_type : MessageType + The message type. + payload_type : PayloadType + The payload type. + address : int + The address of the register that the message will interact with. + value: int | list[int] | float | list[float], optional + The payload of the message. If message_type == MessageType.WRITE, the value cannot be None + """ + if message_type in [MessageType.WRITE, MessageType.EVENT] and value is None: + raise HarpException( + "The value cannot be None if the message type is equal to MessageType.WRITE!" + ) + + self._frame = bytearray() + payload = bytearray() + + if value is not None: + if isinstance(value, int) or isinstance(value, float): + values = [value] + else: + values = value + + for val in values: + if isinstance(val, float): + payload += struct.pack(" int: """ Calculates the checksum of the Harp message. @@ -480,103 +534,3 @@ def payload_as_string(self) -> str: The payload parsed as a str """ return self._raw_payload.decode("utf-8").rstrip("\x00") - - -class ReadHarpMessage(HarpMessage): - """ - A read Harp message sent to a Harp device. - """ - - MESSAGE_TYPE: int = MessageType.READ - - def __init__(self, payload_type: PayloadType, address: int): - self._frame = bytearray() - - self._frame.append(self.MESSAGE_TYPE) - - length: int = 4 - self._frame.append(length) - self._frame.append(address) - self._frame.append(self._port) - self._frame.append(payload_type) - self._frame.append(self.calculate_checksum()) - - -class WriteHarpMessage(HarpMessage): - """ - A write Harp message sent to a Harp device. - - Attributes - ---------- - payload : Union[int, list[int]] - The payload sent in the write Harp message - """ - - MESSAGE_TYPE: int = MessageType.WRITE - - # Define payload type properties - _PAYLOAD_CONFIG = { - # payload_type: (byte_size, signed, is_float) - PayloadType.U8: (1, False), - PayloadType.S8: (1, True), - PayloadType.U16: (2, False), - PayloadType.S16: (2, True), - PayloadType.U32: (4, False), - PayloadType.S32: (4, True), - PayloadType.U64: (8, False), - PayloadType.S64: (8, True), - PayloadType.Float: (4, False), - } - - def __init__( - self, - payload_type: PayloadType, - address: int, - value: int | float | list[int] | list[float], - ): - """ - Create a WriteHarpMessage to send to a device. - - Parameters - ---------- - payload_type : PayloadType - Type of payload (U8, S8, U16, etc.) - address : int - Register address to write to - value : int, float, List[int], or List[float], optional - Value(s) to write - can be a single value or list of values - - Note - ----- - The message frame is constructed according to the HARP binary protocol. - The length is calculated as BASE_LENGTH + payload size in bytes. - """ - - self._frame = bytearray() - - # Get configuration for this payload type - byte_size, signed = self._PAYLOAD_CONFIG.get(payload_type, (1, False)) - - # Convert value to payload bytes - payload = bytearray() - - if isinstance(value, int) or isinstance(value, float): - values = [value] - else: - values = value - - for val in values: - if isinstance(val, float): - payload += struct.pack(" None: - message = ReadHarpMessage(payload_type=PayloadType.U8, address=DEFAULT_ADDRESS) + message = HarpMessage(MessageType.READ, PayloadType.U8, DEFAULT_ADDRESS) assert message.message_type == MessageType.READ assert message.checksum == 47 # 1 + 4 + 42 + 255 + 1 - 256 def test_create_read_S8() -> None: - message = ReadHarpMessage(payload_type=PayloadType.S8, address=DEFAULT_ADDRESS) + message = HarpMessage(MessageType.READ, PayloadType.S8, DEFAULT_ADDRESS) assert message.message_type == MessageType.READ assert message.checksum == 175 # 1 + 4 + 42 + 255 + 129 - 256 def test_create_read_U16() -> None: - message = ReadHarpMessage(payload_type=PayloadType.U16, address=DEFAULT_ADDRESS) + message = HarpMessage(MessageType.READ, PayloadType.U16, DEFAULT_ADDRESS) assert message.message_type == MessageType.READ assert message.checksum == 48 # 1 + 4 + 42 + 255 + 2 - 256 def test_create_read_S16() -> None: - message = ReadHarpMessage(payload_type=PayloadType.S16, address=DEFAULT_ADDRESS) + message = HarpMessage(MessageType.READ, PayloadType.S16, DEFAULT_ADDRESS) assert message.message_type == MessageType.READ assert message.checksum == 176 # 1 + 4 + 42 + 255 + 130 - 256 def test_create_read_U32() -> None: - message = ReadHarpMessage(payload_type=PayloadType.U32, address=DEFAULT_ADDRESS) + message = HarpMessage(MessageType.READ, PayloadType.U32, DEFAULT_ADDRESS) assert message.message_type == MessageType.READ assert message.checksum == 50 # 1 + 4 + 42 + 255 + 4 - 256 def test_create_read_S32() -> None: - message = ReadHarpMessage(payload_type=PayloadType.S32, address=DEFAULT_ADDRESS) + message = HarpMessage(MessageType.READ, PayloadType.S32, DEFAULT_ADDRESS) assert message.message_type == MessageType.READ assert message.checksum == 178 # 1 + 4 + 42 + 255 + 130 - 256 def test_create_read_U64() -> None: - message = ReadHarpMessage(payload_type=PayloadType.U64, address=DEFAULT_ADDRESS) + message = HarpMessage(MessageType.READ, PayloadType.U64, DEFAULT_ADDRESS) assert message.message_type == MessageType.READ assert message.checksum == 54 # 1 + 4 + 42 + 255 + 2 - 256 def test_create_read_S64() -> None: - message = ReadHarpMessage(payload_type=PayloadType.S64, address=DEFAULT_ADDRESS) + message = HarpMessage(MessageType.READ, PayloadType.S64, DEFAULT_ADDRESS) assert message.message_type == MessageType.READ assert message.checksum == 182 # 1 + 4 + 42 + 255 + 130 - 256 def test_create_read_float() -> None: - message = ReadHarpMessage(payload_type=PayloadType.FLOAT, address=DEFAULT_ADDRESS) + message = HarpMessage(MessageType.READ, PayloadType.FLOAT, DEFAULT_ADDRESS) assert message.message_type == MessageType.READ assert message.checksum == 114 # 1 + 4 + 42 + 255 + 4 - 256 @@ -169,7 +162,7 @@ def test_create_read_float() -> None: def test_create_write_U8() -> None: value: int = 23 - message = WriteHarpMessage(PayloadType.U8, DEFAULT_ADDRESS, value) + message = HarpMessage(MessageType.WRITE, PayloadType.U8, DEFAULT_ADDRESS, value) assert message.message_type == MessageType.WRITE assert message.payload == value @@ -178,7 +171,7 @@ def test_create_write_U8() -> None: def test_create_write_S8() -> None: value: int = -3 # corresponds to signed int 253 (0xFD) - message = WriteHarpMessage(PayloadType.S8, DEFAULT_ADDRESS, value) + message = HarpMessage(MessageType.WRITE, PayloadType.S8, DEFAULT_ADDRESS, value) assert message.message_type == MessageType.WRITE assert message.payload == value @@ -187,7 +180,7 @@ def test_create_write_S8() -> None: def test_create_write_U16() -> None: value: int = 1024 # 4 0 (2 x bytes) - message = WriteHarpMessage(PayloadType.U16, DEFAULT_ADDRESS, value) + message = HarpMessage(MessageType.WRITE, PayloadType.U16, DEFAULT_ADDRESS, value) assert message.message_type == MessageType.WRITE assert message.length == 6 @@ -197,7 +190,7 @@ def test_create_write_U16() -> None: def test_create_write_S16() -> None: value: int = -4837 # 27 237 (2 x bytes), corresponds to signed int 7149 - message = WriteHarpMessage(PayloadType.S16, DEFAULT_ADDRESS, value) + message = HarpMessage(MessageType.WRITE, PayloadType.S16, DEFAULT_ADDRESS, value) assert message.message_type == MessageType.WRITE assert message.length == 6 @@ -207,7 +200,7 @@ def test_create_write_S16() -> None: def test_create_write_U8_array() -> None: values: list[int] = [1, 2, 3, 4, 5] - message = WriteHarpMessage(PayloadType.U8, DEFAULT_ADDRESS, values) + message = HarpMessage(MessageType.WRITE, PayloadType.U8, DEFAULT_ADDRESS, values) assert message.message_type == MessageType.WRITE assert message.length == 4 + len( @@ -219,7 +212,7 @@ def test_create_write_U8_array() -> None: def test_create_write_S8_array() -> None: values: list[int] = [-1, -2, -3, -4, -5] - message = WriteHarpMessage(PayloadType.S8, DEFAULT_ADDRESS, values) + message = HarpMessage(MessageType.WRITE, PayloadType.S8, DEFAULT_ADDRESS, values) assert message.message_type == MessageType.WRITE assert message.length == 4 + len( @@ -231,7 +224,7 @@ def test_create_write_S8_array() -> None: def test_create_write_U16_array() -> None: values: list[int] = [1, 2, 3, 4, 5] - message = WriteHarpMessage(PayloadType.U16, DEFAULT_ADDRESS, values) + message = HarpMessage(MessageType.WRITE, PayloadType.U16, DEFAULT_ADDRESS, values) assert message.message_type == MessageType.WRITE assert ( @@ -243,7 +236,7 @@ def test_create_write_U16_array() -> None: def test_create_write_S16_array() -> None: values: list[int] = [-1, -2, -3, -4, -5] - message = WriteHarpMessage(PayloadType.S16, DEFAULT_ADDRESS, values) + message = HarpMessage(MessageType.WRITE, PayloadType.S16, DEFAULT_ADDRESS, values) assert message.message_type == MessageType.WRITE assert ( @@ -255,7 +248,7 @@ def test_create_write_S16_array() -> None: def test_create_write_U32_array() -> None: values: list[int] = [1, 2, 3, 4, 5] - message = WriteHarpMessage(PayloadType.U32, DEFAULT_ADDRESS, values) + message = HarpMessage(MessageType.WRITE, PayloadType.U32, DEFAULT_ADDRESS, values) assert message.message_type == MessageType.WRITE assert ( @@ -267,7 +260,7 @@ def test_create_write_U32_array() -> None: def test_create_write_S32_array() -> None: values: list[int] = [-1, -2, -3, -4, -5] - message = WriteHarpMessage(PayloadType.S32, DEFAULT_ADDRESS, values) + message = HarpMessage(MessageType.WRITE, PayloadType.S32, DEFAULT_ADDRESS, values) assert message.message_type == MessageType.WRITE assert ( @@ -279,7 +272,7 @@ def test_create_write_S32_array() -> None: def test_create_write_U64_array() -> None: values: list[int] = [1, 2, 3, 4, 5] - message = WriteHarpMessage(PayloadType.U64, DEFAULT_ADDRESS, values) + message = HarpMessage(MessageType.WRITE, PayloadType.U64, DEFAULT_ADDRESS, values) assert message.message_type == MessageType.WRITE assert ( @@ -291,7 +284,7 @@ def test_create_write_U64_array() -> None: def test_create_write_S64_array() -> None: values: list[int] = [-1, -2, -3, -4, -5] - message = WriteHarpMessage(PayloadType.S64, DEFAULT_ADDRESS, values) + message = HarpMessage(MessageType.WRITE, PayloadType.S64, DEFAULT_ADDRESS, values) assert message.message_type == MessageType.WRITE assert ( @@ -304,7 +297,7 @@ def test_create_write_S64_array() -> None: def test_create_write_float_array() -> None: """Test creating a write message with float array values.""" values = [1.1, 2.2, 3.3] - message = WriteHarpMessage(PayloadType.FLOAT, DEFAULT_ADDRESS, values) + message = HarpMessage(MessageType.WRITE, PayloadType.FLOAT, DEFAULT_ADDRESS, values) assert message.message_type == MessageType.WRITE expected_checksum = 193 # (2 + 4 + 42 + 255 + 1 + 3 * 4) & 255 @@ -315,9 +308,7 @@ def test_create_write_float_array() -> None: def test_read_who_am_i() -> None: - message = ReadHarpMessage( - payload_type=PayloadType.U16, address=CommonRegisters.WHO_AM_I - ) + message = HarpMessage(MessageType.READ, PayloadType.U16, CommonRegisters.WHO_AM_I) assert str(message.frame) == str(bytearray(b"\x01\x04\x00\xff\x02\x06")) @@ -325,7 +316,7 @@ def test_read_who_am_i() -> None: def test_create_write_U32() -> None: """Test creating a write message with S32 value.""" value: int = 2147483000 # Large number - message = WriteHarpMessage(PayloadType.U32, DEFAULT_ADDRESS, value) + message = HarpMessage(MessageType.WRITE, PayloadType.U32, DEFAULT_ADDRESS, value) assert message.message_type == MessageType.WRITE assert message.length == 8 @@ -339,7 +330,7 @@ def test_create_write_U32() -> None: def test_create_write_S32() -> None: """Test creating a write message with S32 value.""" value: int = -2147483000 # Large negative number - message = WriteHarpMessage(PayloadType.S32, DEFAULT_ADDRESS, value) + message = HarpMessage(MessageType.WRITE, PayloadType.S32, DEFAULT_ADDRESS, value) assert message.message_type == MessageType.WRITE assert message.length == 8 @@ -353,7 +344,7 @@ def test_create_write_S32() -> None: def test_create_write_U64() -> None: """Test creating a write message with U64 value.""" value: int = 9223372036854775807 # Large 64-bit value - message = WriteHarpMessage(PayloadType.U64, DEFAULT_ADDRESS, value) + message = HarpMessage(MessageType.WRITE, PayloadType.U64, DEFAULT_ADDRESS, value) assert message.message_type == MessageType.WRITE assert message.length == 12 # 5 header bytes + 8 payload bytes @@ -367,7 +358,7 @@ def test_create_write_U64() -> None: def test_create_write_S64() -> None: """Test creating a write message with S64 value.""" value: int = -9223372036854775807 - message = WriteHarpMessage(PayloadType.S64, DEFAULT_ADDRESS, value) + message = HarpMessage(MessageType.WRITE, PayloadType.S64, DEFAULT_ADDRESS, value) assert message.message_type == MessageType.WRITE assert message.length == 12 assert message.payload == value @@ -535,7 +526,7 @@ def test_reply_without_timestamp_raises() -> None: def test_calculate_checksum() -> None: """Test the calculate_checksum method.""" - message = HarpMessage() + message = HarpMessage(MessageType.READ, PayloadType.U8, DEFAULT_ADDRESS) message._frame = bytearray([1, 2, 3, 4, 5]) # Sum is 15, checksum is 15 (no overflow) From ed8fca5a7b5d559dce21523e65218c25a8b26efe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Thu, 9 Oct 2025 09:36:20 +0100 Subject: [PATCH 165/267] Remove HarpMessage.create static method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- .../olfactometer_example.py | 39 +-- .../read_and_write_from_registers.py | 13 +- src/harp-protocol/harp/protocol/messages.py | 34 --- src/harp-serial/README.md | 8 +- src/harp-serial/harp/serial/device.py | 247 +++++------------- tests/test_device.py | 6 +- 6 files changed, 98 insertions(+), 249 deletions(-) diff --git a/docs/examples/olfactometer_example/olfactometer_example.py b/docs/examples/olfactometer_example/olfactometer_example.py index b4ac526..74f089b 100644 --- a/docs/examples/olfactometer_example/olfactometer_example.py +++ b/docs/examples/olfactometer_example/olfactometer_example.py @@ -2,10 +2,11 @@ import time from threading import Event, Thread +from serial import SerialException + from harp.protocol import MessageType, PayloadType from harp.protocol.messages import HarpMessage from harp.serial.device import Device, OperationMode -from serial import SerialException SERIAL_PORT = ( "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port) @@ -41,7 +42,7 @@ def main(): device.set_mode(OperationMode.ACTIVE) # Enable flow - device.send(HarpMessage.create(MessageType.WRITE, 32, PayloadType.U8, 0x01)) + device.send(HarpMessage(MessageType.WRITE, PayloadType.U8, 32, 0x01)) # Initialize thread for events events_thread = Thread( @@ -55,52 +56,52 @@ def main(): # Set the valves to a random flow device.send( - HarpMessage.create( - MessageType.WRITE, 42, PayloadType.Float, int(random.random() * 100) + HarpMessage( + MessageType.WRITE, PayloadType.FLOAT, 42, int(random.random() * 100) ) ) device.send( - HarpMessage.create( - MessageType.WRITE, 43, PayloadType.Float, int(random.random() * 100) + HarpMessage( + MessageType.WRITE, PayloadType.FLOAT, 43, int(random.random() * 100) ) ) device.send( - HarpMessage.create( - MessageType.WRITE, 44, PayloadType.Float, int(random.random() * 100) + HarpMessage( + MessageType.WRITE, PayloadType.FLOAT, 44, int(random.random() * 100) ) ) device.send( - HarpMessage.create( - MessageType.WRITE, 45, PayloadType.Float, int(random.random() * 100) + HarpMessage( + MessageType.WRITE, PayloadType.FLOAT, 45, int(random.random() * 100) ) ) # Open every odor valve, one at a time every 5 seconds - device.send(HarpMessage.create(MessageType.WRITE, 68, PayloadType.Float, 0x01)) + device.send(HarpMessage(MessageType.WRITE, PayloadType.FLOAT, 68, 0x01)) time.sleep(5) - device.send(HarpMessage.create(MessageType.WRITE, 69, PayloadType.Float, 0x01)) - device.send(HarpMessage.create(MessageType.WRITE, 68, PayloadType.Float, 0x02)) + device.send(HarpMessage(MessageType.WRITE, PayloadType.FLOAT, 69, 0x01)) + device.send(HarpMessage(MessageType.WRITE, PayloadType.FLOAT, 68, 0x02)) time.sleep(5) - device.send(HarpMessage.create(MessageType.WRITE, 69, PayloadType.Float, 0x02)) - device.send(HarpMessage.create(MessageType.WRITE, 68, PayloadType.Float, 0x04)) + device.send(HarpMessage(MessageType.WRITE, PayloadType.FLOAT, 69, 0x02)) + device.send(HarpMessage(MessageType.WRITE, PayloadType.FLOAT, 68, 0x04)) time.sleep(5) - device.send(HarpMessage.create(MessageType.WRITE, 69, PayloadType.Float, 0x04)) - device.send(HarpMessage.create(MessageType.WRITE, 68, PayloadType.Float, 0x08)) + device.send(HarpMessage(MessageType.WRITE, PayloadType.FLOAT, 69, 0x04)) + device.send(HarpMessage(MessageType.WRITE, PayloadType.FLOAT, 68, 0x08)) time.sleep(5) - device.send(HarpMessage.create(MessageType.WRITE, 69, PayloadType.Float, 0x08)) + device.send(HarpMessage(MessageType.WRITE, PayloadType.FLOAT, 69, 0x08)) time.sleep(5) # Disable flow - device.send(HarpMessage.create(MessageType.WRITE, 32, PayloadType.Float, 0x00)) + device.send(HarpMessage(MessageType.WRITE, PayloadType.FLOAT, 32, 0x00)) time.sleep(1) diff --git a/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py b/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py index 7533fe1..efa4664 100755 --- a/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py +++ b/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py @@ -1,7 +1,8 @@ +from serial import SerialException + from harp.protocol import MessageType, PayloadType from harp.protocol.messages import HarpMessage from harp.serial.device import Device -from serial import SerialException SERIAL_PORT = ( "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port) @@ -15,17 +16,17 @@ raise SerialException("This is not a Harp Behavior.") # Read initial DI3 state -reply = device.send(HarpMessage.create(MessageType.READ, 32, PayloadType.U8)) +reply = device.send(HarpMessage(MessageType.READ, PayloadType.U8, 32)) print(reply.payload & 0x08) # Turn DO0 on and read DI3 state after it -reply = device.send(HarpMessage.create(MessageType.READ, 34, PayloadType.U8, 0x400)) -reply = device.send(HarpMessage.create(MessageType.READ, 32, PayloadType.U8)) +reply = device.send(HarpMessage(MessageType.READ, PayloadType.U8, 34, 0x400)) +reply = device.send(HarpMessage(MessageType.READ, PayloadType.U8, 32)) print(reply.payload & 0x08) # Turn DO0 off and read DI3 state again -reply = device.send(HarpMessage.create(MessageType.READ, 35, PayloadType.U8, 0x400)) -reply = device.send(HarpMessage.create(MessageType.READ, 32, PayloadType.U8)) +reply = device.send(HarpMessage(MessageType.READ, PayloadType.U8, 35, 0x400)) +reply = device.send(HarpMessage(MessageType.READ, PayloadType.U8, 32)) print(reply.payload & 0x08) # Close connection diff --git a/src/harp-protocol/harp/protocol/messages.py b/src/harp-protocol/harp/protocol/messages.py index 4d95a74..19e4823 100644 --- a/src/harp-protocol/harp/protocol/messages.py +++ b/src/harp-protocol/harp/protocol/messages.py @@ -374,40 +374,6 @@ def parse(frame: bytearray) -> ReplyHarpMessage: """ return ReplyHarpMessage(frame) - @staticmethod - def create( - message_type: MessageType, - address: int, - payload_type: PayloadType, - value: Optional[int | list[int] | float | list[float]] = None, - ) -> HarpMessage: - """ - Creates a Harp message. - - Parameters - ---------- - message_type : MessageType - The message type. It can only be of type READ or WRITE - address : int - The address of the register that the message will interact with - payload_type : PayloadType - The payload type - value: int | list[int] | float | list[float], optional - The payload of the message. If message_type == MessageType.WRITE, the value cannot be None - """ - if message_type == MessageType.READ: - return ReadHarpMessage(payload_type, address) - elif message_type == MessageType.WRITE and value is not None: - return WriteHarpMessage(payload_type, address, value) - elif message_type != MessageType.READ and message_type != MessageType.WRITE: - raise Exception( - "The only valid message types are MessageType.READ and MessageType.Write!" - ) - else: - raise Exception( - "The value cannot be None if the message type is equal to MessageType.WRITE!" - ) - def __repr__(self) -> str: """ Prints debug representation of the reply message. diff --git a/src/harp-serial/README.md b/src/harp-serial/README.md index d41a8f9..e9608bb 100644 --- a/src/harp-serial/README.md +++ b/src/harp-serial/README.md @@ -30,10 +30,10 @@ device.info() register_address = 32 # Read from register -reply = device.send(HarpMessage.create(MessageType.READ, register_address, PayloadType.U8)) +reply = device.send(HarpMessage(MessageType.READ, PayloadType.U8, register_address)) # Write to register -device.send(HarpMessage.create(MessageType.WRITE, register_address, PayloadType.U8, reply.payload)) +device.send(HarpMessage(MessageType.WRITE, PayloadType.U8, register_address, reply.payload)) # Disconnect when done device.disconnect() @@ -54,10 +54,10 @@ with Device("/dev/ttyUSB0") as device: register_address = 32 # Read from register - reply = device.send(HarpMessage.create(MessageType.READ, register_address, PayloadType.U8)) + reply = device.send(HarpMessage(MessageType.READ, PayloadType.U8, register_address)) # Write to register - device.send(HarpMessage.create(MessageType.WRITE, register_address, PayloadType.U8, reply.payload)) + device.send(HarpMessage(MessageType.WRITE, PayloadType.U8, register_address, reply.payload)) ``` ## for Linux diff --git a/src/harp-serial/harp/serial/device.py b/src/harp-serial/harp/serial/device.py index 77468b4..262e272 100644 --- a/src/harp-serial/harp/serial/device.py +++ b/src/harp-serial/harp/serial/device.py @@ -200,7 +200,7 @@ def _read_device_mode(self) -> OperationMode: The current device mode """ address = CommonRegisters.OPERATION_CTRL - reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8)) + reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) return OperationMode(reply.payload & OperationCtrl.OP_MODE) def dump_registers(self) -> list: @@ -214,9 +214,7 @@ def dump_registers(self) -> list: The list containing the reply Harp messages for all the device's registers """ address = CommonRegisters.OPERATION_CTRL - reg_value = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8) - ) + reg_value = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) if reg_value is None: return [] @@ -225,9 +223,7 @@ def dump_registers(self) -> list: # Assert DUMP bit reg_value |= OperationCtrl.DUMP - self.send( - HarpMessage.create(MessageType.WRITE, address, PayloadType.U8, reg_value) - ) + self.send(HarpMessage(MessageType.WRITE, PayloadType.U8, address, reg_value)) # Receive the contents of all registers as Harp Read Reply Messages replies = [] @@ -249,7 +245,7 @@ def read_operation_ctrl(self): The reply to the Harp message """ address = CommonRegisters.OPERATION_CTRL - reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8)) + reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) # create dict with complete byte and then decode each bit according to the OperationCtrl entries if reply is not None: @@ -296,14 +292,12 @@ def write_operation_ctrl( address = CommonRegisters.OPERATION_CTRL # Read register first - reg_value = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8) - ) + reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) - if reg_value is None: - return reg_value + if reply is None: + return reply - reg_value = reg_value.payload + reg_value = reply.payload if mode is not None: # Clear old operation mode @@ -336,7 +330,7 @@ def write_operation_ctrl( reg_value &= ~OperationCtrl.ALIVE_EN reply = self.send( - HarpMessage.create(MessageType.WRITE, address, PayloadType.U8, reg_value) + HarpMessage(MessageType.WRITE, PayloadType.U8, address, reg_value) ) return reply @@ -358,14 +352,12 @@ def set_mode(self, mode: OperationMode) -> ReplyHarpMessage | None: address = CommonRegisters.OPERATION_CTRL # Read register first - reg_value = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8) - ) + reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) - if reg_value is None: - return reg_value + if reply is None: + return reply - reg_value = reg_value.payload + reg_value = reply.payload # Clear old operation mode reg_value &= ~OperationCtrl.OP_MODE @@ -373,7 +365,7 @@ def set_mode(self, mode: OperationMode) -> ReplyHarpMessage | None: # Set new operation mode reg_value |= mode reply = self.send( - HarpMessage.create(MessageType.WRITE, address, PayloadType.U8, reg_value) + HarpMessage(MessageType.WRITE, PayloadType.U8, address, reg_value) ) return reply @@ -395,14 +387,12 @@ def alive_en(self, enable: bool) -> bool: address = CommonRegisters.OPERATION_CTRL # Read register first - reg_value = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8) - ) + reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) - if reg_value is None: + if reply is None: return False - reg_value = reg_value.payload + reg_value = reply.payload if enable: reg_value |= OperationCtrl.ALIVE_EN @@ -410,7 +400,7 @@ def alive_en(self, enable: bool) -> bool: reg_value &= ~OperationCtrl.ALIVE_EN reply = self.send( - HarpMessage.create(MessageType.WRITE, address, PayloadType.U8, reg_value) + HarpMessage(MessageType.WRITE, PayloadType.U8, address, reg_value) ) if reply is None: @@ -435,14 +425,12 @@ def op_led_en(self, enable: bool) -> bool: address = CommonRegisters.OPERATION_CTRL # Read register first - reg_value = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8) - ) + reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) - if reg_value is None: + if reply is None: return False - reg_value = reg_value.payload + reg_value = reply.payload if enable: reg_value |= OperationCtrl.OPLEDEN @@ -450,7 +438,7 @@ def op_led_en(self, enable: bool) -> bool: reg_value &= ~OperationCtrl.OPLEDEN reply = self.send( - HarpMessage.create(MessageType.WRITE, address, PayloadType.U8, reg_value) + HarpMessage(MessageType.WRITE, PayloadType.U8, address, reg_value) ) return reply is not None @@ -472,14 +460,12 @@ def visual_en(self, enable: bool) -> bool: address = CommonRegisters.OPERATION_CTRL # Read register first - reg_value = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8) - ) + reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) - if reg_value is None: + if reply is None: return False - reg_value = reg_value.payload + reg_value = reply.payload if enable: reg_value |= OperationCtrl.VISUALEN @@ -487,7 +473,7 @@ def visual_en(self, enable: bool) -> bool: reg_value &= ~OperationCtrl.VISUALEN reply = self.send( - HarpMessage.create(MessageType.WRITE, address, PayloadType.U8, reg_value) + HarpMessage(MessageType.WRITE, PayloadType.U8, address, reg_value) ) return reply is not None @@ -509,14 +495,12 @@ def mute_reply(self, enable: bool) -> bool: address = CommonRegisters.OPERATION_CTRL # Read register first - reg_value = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U8) - ) + reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) - if reg_value is None: + if reply is None: return False - reg_value = reg_value.payload + reg_value = reply.payload if enable: reg_value |= OperationCtrl.MUTE_RPL @@ -524,7 +508,7 @@ def mute_reply(self, enable: bool) -> bool: reg_value &= ~OperationCtrl.MUTE_RPL reply = self.send( - HarpMessage.create(MessageType.WRITE, address, PayloadType.U8, reg_value) + HarpMessage(MessageType.WRITE, PayloadType.U8, address, reg_value) ) return reply is not None @@ -542,7 +526,7 @@ def reset_device( """ address = CommonRegisters.RESET_DEV reply = self.send( - HarpMessage.create(MessageType.WRITE, address, PayloadType.U8, reset_mode) + HarpMessage(MessageType.WRITE, PayloadType.U8, address, reset_mode) ) return reply @@ -563,7 +547,7 @@ def set_clock_config(self, clock_config: ClockConfig) -> ReplyHarpMessage | None """ address = CommonRegisters.CLOCK_CONFIG reply = self.send( - HarpMessage.create(MessageType.WRITE, address, PayloadType.U8, clock_config) + HarpMessage(MessageType.WRITE, PayloadType.U8, address, clock_config) ) return reply @@ -584,9 +568,7 @@ def set_timestamp_offset(self, timestamp_offset: int) -> ReplyHarpMessage | None """ address = CommonRegisters.TIMESTAMP_OFFSET reply = self.send( - HarpMessage.create( - MessageType.WRITE, address, PayloadType.U8, timestamp_offset - ) + HarpMessage(MessageType.WRITE, PayloadType.U8, address, timestamp_offset) ) return reply @@ -716,13 +698,7 @@ def read_u8(self, address: int) -> ReplyHarpMessage | None: HarpTimeoutError If no reply is received and the effective strategy requires raising """ - reply = self.send( - HarpMessage.create( - message_type=MessageType.READ, - address=address, - payload_type=PayloadType.U8, - ) - ) + reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) return reply @@ -745,13 +721,7 @@ def read_s8(self, address: int) -> ReplyHarpMessage | None: HarpTimeoutError If no reply is received and the effective strategy requires raising """ - reply = self.send( - HarpMessage.create( - message_type=MessageType.READ, - address=address, - payload_type=PayloadType.S8, - ) - ) + reply = self.send(HarpMessage(MessageType.READ, PayloadType.S8, address)) return reply @@ -774,13 +744,7 @@ def read_u16(self, address: int) -> ReplyHarpMessage | None: HarpTimeoutError If no reply is received and the effective strategy requires raising """ - reply = self.send( - HarpMessage.create( - message_type=MessageType.READ, - address=address, - payload_type=PayloadType.U16, - ) - ) + reply = self.send(HarpMessage(MessageType.READ, PayloadType.U16, address)) return reply @@ -803,13 +767,7 @@ def read_s16(self, address: int) -> ReplyHarpMessage | None: HarpTimeoutError If no reply is received and the effective strategy requires raising """ - reply = self.send( - HarpMessage.create( - message_type=MessageType.READ, - address=address, - payload_type=PayloadType.S16, - ) - ) + reply = self.send(HarpMessage(MessageType.READ, PayloadType.S16, address)) return reply @@ -832,13 +790,7 @@ def read_u32(self, address: int) -> ReplyHarpMessage | None: HarpTimeoutError If no reply is received and the effective strategy requires raising """ - reply = self.send( - HarpMessage.create( - message_type=MessageType.READ, - address=address, - payload_type=PayloadType.U32, - ) - ) + reply = self.send(HarpMessage(MessageType.READ, PayloadType.U32, address)) return reply @@ -861,13 +813,7 @@ def read_s32(self, address: int) -> ReplyHarpMessage | None: HarpTimeoutError If no reply is received and the effective strategy requires raising """ - reply = self.send( - HarpMessage.create( - message_type=MessageType.READ, - address=address, - payload_type=PayloadType.S32, - ) - ) + reply = self.send(HarpMessage(MessageType.READ, PayloadType.S32, address)) return reply @@ -890,13 +836,7 @@ def read_u64(self, address: int) -> ReplyHarpMessage | None: HarpTimeoutError If no reply is received and the effective strategy requires raising """ - reply = self.send( - HarpMessage.create( - message_type=MessageType.READ, - address=address, - payload_type=PayloadType.U64, - ) - ) + reply = self.send(HarpMessage(MessageType.READ, PayloadType.U64, address)) return reply @@ -919,13 +859,7 @@ def read_s64(self, address: int) -> ReplyHarpMessage | None: HarpTimeoutError If no reply is received and the effective strategy requires raising """ - reply = self.send( - HarpMessage.create( - message_type=MessageType.READ, - address=address, - payload_type=PayloadType.S64, - ) - ) + reply = self.send(HarpMessage(MessageType.READ, PayloadType.S64, address)) return reply @@ -948,13 +882,7 @@ def read_float(self, address: int) -> ReplyHarpMessage | None: HarpTimeoutError If no reply is received and the effective strategy requires raising """ - reply = self.send( - HarpMessage.create( - message_type=MessageType.READ, - address=address, - payload_type=PayloadType.Float, - ) - ) + reply = self.send(HarpMessage(MessageType.READ, PayloadType.FLOAT, address)) return reply @@ -980,12 +908,7 @@ def write_u8(self, address: int, value: int | list[int]) -> ReplyHarpMessage | N If no reply is received and the effective strategy requires raising """ reply = self.send( - HarpMessage.create( - message_type=MessageType.WRITE, - address=address, - payload_type=PayloadType.U8, - value=value, - ) + HarpMessage(MessageType.WRITE, PayloadType.U8, address, value) ) return reply @@ -1012,12 +935,7 @@ def write_s8(self, address: int, value: int | list[int]) -> ReplyHarpMessage | N If no reply is received and the effective strategy requires raising """ reply = self.send( - HarpMessage.create( - message_type=MessageType.WRITE, - address=address, - payload_type=PayloadType.S8, - value=value, - ) + HarpMessage(MessageType.WRITE, PayloadType.S8, address, value) ) return reply @@ -1046,12 +964,7 @@ def write_u16( If no reply is received and the effective strategy requires raising """ reply = self.send( - HarpMessage.create( - message_type=MessageType.WRITE, - address=address, - payload_type=PayloadType.U16, - value=value, - ) + HarpMessage(MessageType.WRITE, PayloadType.U16, address, value) ) return reply @@ -1080,12 +993,7 @@ def write_s16( If no reply is received and the effective strategy requires raising """ reply = self.send( - HarpMessage.create( - message_type=MessageType.WRITE, - address=address, - payload_type=PayloadType.S16, - value=value, - ) + HarpMessage(MessageType.WRITE, PayloadType.S16, address, value) ) return reply @@ -1114,12 +1022,7 @@ def write_u32( If no reply is received and the effective strategy requires raising """ reply = self.send( - HarpMessage.create( - message_type=MessageType.WRITE, - address=address, - payload_type=PayloadType.U32, - value=value, - ) + HarpMessage(MessageType.WRITE, PayloadType.U32, address, value) ) return reply @@ -1148,12 +1051,7 @@ def write_s32( If no reply is received and the effective strategy requires raising """ reply = self.send( - HarpMessage.create( - message_type=MessageType.WRITE, - address=address, - payload_type=PayloadType.S32, - value=value, - ) + HarpMessage(MessageType.WRITE, PayloadType.S32, address, value) ) return reply @@ -1182,12 +1080,7 @@ def write_u64( If no reply is received and the effective strategy requires raising """ reply = self.send( - HarpMessage.create( - message_type=MessageType.WRITE, - address=address, - payload_type=PayloadType.U64, - value=value, - ) + HarpMessage(MessageType.WRITE, PayloadType.U64, address, value) ) return reply @@ -1216,12 +1109,7 @@ def write_s64( If no reply is received and the effective strategy requires raising """ reply = self.send( - HarpMessage.create( - message_type=MessageType.WRITE, - address=address, - payload_type=PayloadType.S64, - value=value, - ) + HarpMessage(MessageType.WRITE, PayloadType.S64, address, value) ) return reply @@ -1250,12 +1138,7 @@ def write_float( If no reply is received and the effective strategy requires raising """ reply = self.send( - HarpMessage.create( - message_type=MessageType.WRITE, - address=address, - payload_type=PayloadType.Float, - value=value, - ) + HarpMessage(MessageType.WRITE, PayloadType.FLOAT, address, value) ) return reply @@ -1271,9 +1154,7 @@ def _read_who_am_i(self) -> int: """ address = CommonRegisters.WHO_AM_I - reply = self.send( - HarpMessage.create(MessageType.READ, address, PayloadType.U16) - ) + reply = self.send(HarpMessage(MessageType.READ, PayloadType.U16, address)) return reply.payload @@ -1299,7 +1180,7 @@ def _read_hw_version_h(self) -> int: """ address = CommonRegisters.HW_VERSION_H - reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8)) + reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) return reply.payload @@ -1314,7 +1195,7 @@ def _read_hw_version_l(self) -> int: """ address = CommonRegisters.HW_VERSION_L - reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8)) + reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) return reply.payload @@ -1329,7 +1210,7 @@ def _read_assembly_version(self) -> int: """ address = CommonRegisters.ASSEMBLY_VERSION - reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8)) + reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) return reply.payload @@ -1344,7 +1225,7 @@ def _read_core_version_h(self) -> int: """ address = CommonRegisters.CORE_VERSION_H - reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8)) + reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) return reply.payload @@ -1359,7 +1240,7 @@ def _read_core_version_l(self) -> int: """ address = CommonRegisters.CORE_VERSION_L - reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8)) + reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) return reply.payload @@ -1374,7 +1255,7 @@ def _read_fw_version_h(self) -> int: """ address = CommonRegisters.FIRMWARE_VERSION_H - reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8)) + reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) return reply.payload @@ -1389,7 +1270,7 @@ def _read_fw_version_l(self) -> int: """ address = CommonRegisters.FIRMWARE_VERSION_L - reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8)) + reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) return reply.payload @@ -1404,7 +1285,7 @@ def _read_device_name(self) -> str: """ address = CommonRegisters.DEVICE_NAME - reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8)) + reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) return reply.payload_as_string() @@ -1419,9 +1300,9 @@ def _read_serial_number(self) -> int: """ address = CommonRegisters.SERIAL_NUMBER - reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8)) + reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) - if reply.is_error: + if reply is not None and reply.is_error: return 0 return reply.payload @@ -1437,7 +1318,7 @@ def _read_clock_config(self) -> int: """ address = CommonRegisters.CLOCK_CONFIG - reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8)) + reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) return reply.payload @@ -1452,7 +1333,7 @@ def _read_timestamp_offset(self) -> int: """ address = CommonRegisters.TIMESTAMP_OFFSET - reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8)) + reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) return reply.payload diff --git a/tests/test_device.py b/tests/test_device.py index a0137ae..4fe552f 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -25,7 +25,7 @@ # read_size: int = 35 # TODO: automatically calculate this! # reply: ReplyHarpMessage = device.send( -# HarpMessage.create(MessageType.READ, register, PayloadType.U8) +# HarpMessage(MessageType.READ, PayloadType.U8, register) # ) # assert reply is not None # # assert reply.payload == write_value @@ -48,8 +48,8 @@ # # write 65 on register 38 # reply = device.send( -# HarpMessage.create( -# MessageType.WRITE, register, PayloadType.U8, write_value +# HarpMessage( +# MessageType.WRITE, PayloadType.U8, register, write_value # ) # ) # assert reply is not None From 78cce0e8807bad7001179c874d38f89a4d25a65e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Fri, 17 Oct 2025 15:39:48 +0100 Subject: [PATCH 166/267] Refactor HarpMessage to remove ReplyHarpMessage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- docs/api/protocol.md | 1 - src/harp-protocol/harp/protocol/messages.py | 127 ++++++++------------ src/harp-serial/harp/serial/device.py | 118 ++++++++---------- tests/test_device.py | 4 +- tests/test_messages.py | 85 ++++++++----- 5 files changed, 160 insertions(+), 175 deletions(-) diff --git a/docs/api/protocol.md b/docs/api/protocol.md index 2d765f6..a222a56 100644 --- a/docs/api/protocol.md +++ b/docs/api/protocol.md @@ -10,4 +10,3 @@ ::: harp.protocol.ResetMode ::: harp.protocol.ClockConfig ::: harp.protocol.messages.HarpMessage -::: harp.protocol.messages.ReplyHarpMessage diff --git a/src/harp-protocol/harp/protocol/messages.py b/src/harp-protocol/harp/protocol/messages.py index 19e4823..a253522 100644 --- a/src/harp-protocol/harp/protocol/messages.py +++ b/src/harp-protocol/harp/protocol/messages.py @@ -33,6 +33,8 @@ class HarpMessage: BASE_LENGTH: int = 4 _frame: bytearray = bytearray() _port: int = DEFAULT_PORT + _timestamp: Optional[float] = None + _raw_payload: bytearray = bytearray() def __init__( self, @@ -85,6 +87,10 @@ def __init__( if value is not None: self._frame += payload + if self.payload_type.has_timestamp(): + self._raw_payload = self._frame[11:-1] + else: + self._raw_payload = self._frame[5:-1] self._frame.append(self.calculate_checksum()) @@ -186,6 +192,10 @@ def payload_type(self) -> PayloadType: """ return PayloadType(self._frame[4]) + @property + def timestamp(self) -> float | None: + return self._timestamp + @property def payload(self) -> Union[int, list[int], bytearray, float, list[float]]: """ @@ -357,8 +367,31 @@ def checksum(self) -> int: """ return self._frame[-1] + @property + def is_error(self) -> bool: + """ + Indicates if this HarpMessage is an error message or not. + + Returns + ------- + bool + Returns True if this HarpMessage is an error message, False otherwise. + """ + return self.message_type.is_error() + + def payload_as_string(self) -> str: + """ + Returns the payload as a str. + + Returns + ------- + str + The payload parsed as a str + """ + return self._raw_payload.decode("utf-8").rstrip("\x00") + @staticmethod - def parse(frame: bytearray) -> ReplyHarpMessage: + def parse(frame: bytearray) -> HarpMessage: """ Parses a bytearray to a (reply) Harp message. @@ -369,10 +402,25 @@ def parse(frame: bytearray) -> ReplyHarpMessage: Returns ------- - ReplyHarpMessage + HarpMessage The Harp message object parsed from the original bytearray """ - return ReplyHarpMessage(frame) + message = HarpMessage(MessageType(frame[0]), PayloadType(frame[4]), frame[2]) + + message._frame = frame + + # assign timestamp if exists + if message.payload_type.has_timestamp(): + message._raw_payload = frame[11:-1] + message._timestamp = ( + int.from_bytes(frame[5:9], byteorder="little", signed=False) + + int.from_bytes(frame[9:11], byteorder="little", signed=False) * 32e-6 + ) + else: + message._raw_payload = frame[5:-1] + message._timestamp = None + + return message def __repr__(self) -> str: """ @@ -427,76 +475,3 @@ def __str__(self) -> str: + f"Payload: {payload_str}\r\n" + f"Checksum: {self.checksum}" ) - - -class ReplyHarpMessage(HarpMessage): - """ - A response message from a Harp device. - - Attributes - ---------- - payload : Union[int, list[int]] - The message payload formatted as the appropriate type - timestamp : float - The Harp timestamp at which the message was sent - """ - - def __init__( - self, - frame: bytearray, - ): - """ - Parameters - ---------- - frame : bytearray - The Harp message in bytearray format - """ - - self._frame = frame - # Retrieve all content from 11 (where payload starts) until the checksum (not inclusive) - self._raw_payload = frame[11:-1] - - # Assign timestamp after _payload since @properties all rely on self._payload. - self._timestamp = ( - int.from_bytes(frame[5:9], byteorder="little", signed=False) - + int.from_bytes(frame[9:11], byteorder="little", signed=False) * 32e-6 - ) - - # Timestamp is junk if it's not present. - if not self.payload_type.has_timestamp(): - raise HarpReadException(self.address) - - @property - def is_error(self) -> bool: - """ - Indicates if this HarpMessage is an error message or not. - - Returns - ------- - bool - Returns True if this HarpMessage is an error message, False otherwise. - """ - return self.message_type.is_error() - - @property - def timestamp(self) -> float: - """ - The Harp timestamp at which the message was sent. - - Returns - ------- - float - The Harp timestamp at which the message was sent - """ - return self._timestamp - - def payload_as_string(self) -> str: - """ - Returns the payload as a str. - - Returns - ------- - str - The payload parsed as a str - """ - return self._raw_payload.decode("utf-8").rstrip("\x00") diff --git a/src/harp-serial/harp/serial/device.py b/src/harp-serial/harp/serial/device.py index 262e272..a651bca 100644 --- a/src/harp-serial/harp/serial/device.py +++ b/src/harp-serial/harp/serial/device.py @@ -19,7 +19,7 @@ ) from harp.protocol.device_names import device_names from harp.protocol.exceptions import HarpTimeoutError -from harp.protocol.messages import HarpMessage, ReplyHarpMessage +from harp.protocol.messages import HarpMessage from harp.serial.harp_serial import HarpSerial @@ -241,7 +241,7 @@ def read_operation_ctrl(self): Returns ------- - ReplyHarpMessage + HarpMessage The reply to the Harp message """ address = CommonRegisters.OPERATION_CTRL @@ -268,7 +268,7 @@ def write_operation_ctrl( visual_en: Optional[bool] = None, op_led_en: Optional[bool] = None, alive_en: Optional[bool] = None, - ) -> ReplyHarpMessage | None: + ) -> HarpMessage | None: """ Writes the OPERATION_CTRL register of the device. @@ -286,7 +286,7 @@ def write_operation_ctrl( If True, enables the ALIVE_EN bit Returns ------- - ReplyHarpMessage + HarpMessage The reply to the Harp message """ address = CommonRegisters.OPERATION_CTRL @@ -335,7 +335,7 @@ def write_operation_ctrl( return reply - def set_mode(self, mode: OperationMode) -> ReplyHarpMessage | None: + def set_mode(self, mode: OperationMode) -> HarpMessage | None: """ Sets the operation mode of the device. @@ -346,7 +346,7 @@ def set_mode(self, mode: OperationMode) -> ReplyHarpMessage | None: Returns ------- - ReplyHarpMessage + HarpMessage The reply to the Harp message """ address = CommonRegisters.OPERATION_CTRL @@ -515,13 +515,13 @@ def mute_reply(self, enable: bool) -> bool: def reset_device( self, reset_mode: ResetMode = ResetMode.RST_DEF - ) -> ReplyHarpMessage | None: + ) -> HarpMessage | None: """ Resets the device and reboots with all the registers with the default values. Beware that the EEPROM will be erased. More information on the reset device register can be found [here](https://harp-tech.org/protocol/Device.html#r_reset_dev-u8--reset-device-and-save-non-volatile-registers). Returns ------- - ReplyHarpMessage + HarpMessage The reply to the Harp message """ address = CommonRegisters.RESET_DEV @@ -531,7 +531,7 @@ def reset_device( return reply - def set_clock_config(self, clock_config: ClockConfig) -> ReplyHarpMessage | None: + def set_clock_config(self, clock_config: ClockConfig) -> HarpMessage | None: """ Sets the clock configuration of the device. @@ -542,7 +542,7 @@ def set_clock_config(self, clock_config: ClockConfig) -> ReplyHarpMessage | None Returns ------- - ReplyHarpMessage + HarpMessage The reply to the Harp message """ address = CommonRegisters.CLOCK_CONFIG @@ -552,7 +552,7 @@ def set_clock_config(self, clock_config: ClockConfig) -> ReplyHarpMessage | None return reply - def set_timestamp_offset(self, timestamp_offset: int) -> ReplyHarpMessage | None: + def set_timestamp_offset(self, timestamp_offset: int) -> HarpMessage | None: """ When the value of this register is above 0 (zero), the device's timestamp will be offset by this amount. The register is sensitive to 500 microsecond increments. This register is non-volatile. @@ -563,7 +563,7 @@ def set_timestamp_offset(self, timestamp_offset: int) -> ReplyHarpMessage | None Returns ------- - ReplyHarpMessage + HarpMessage The reply to the Harp message """ address = CommonRegisters.TIMESTAMP_OFFSET @@ -579,7 +579,7 @@ def send( *, expect_reply: bool = True, timeout_strategy: TimeoutStrategy | None = None, - ) -> ReplyHarpMessage | None: + ) -> HarpMessage | None: """ Sends a Harp message and (optionally) waits for a reply. @@ -594,7 +594,7 @@ def send( Returns ------- - ReplyHarpMessage | None + HarpMessage | None Reply (or None when allowed by the timeout strategy or expect_reply=False) Raises @@ -625,13 +625,13 @@ def send( self._dump_reply(reply.frame) return reply - def _read(self) -> ReplyHarpMessage: + def _read(self) -> HarpMessage: """ Reads an incoming serial message in a blocking way. Returns ------- - ReplyHarpMessage + HarpMessage The incoming Harp message in case it exists Raises @@ -651,7 +651,7 @@ def _dump_reply(self, reply: bytearray): if self._dump_file: self._dump_file.write(reply) - def get_events(self) -> list[ReplyHarpMessage]: + def get_events(self) -> list[HarpMessage]: """ Gets all events from the event queue. @@ -679,7 +679,7 @@ def event_count(self) -> int: """ return self._ser.event_q.qsize() - def read_u8(self, address: int) -> ReplyHarpMessage | None: + def read_u8(self, address: int) -> HarpMessage | None: """ Reads the value of a register of type U8. @@ -690,7 +690,7 @@ def read_u8(self, address: int) -> ReplyHarpMessage | None: Returns ------- - ReplyHarpMessage + HarpMessage The reply to the Harp message that will contain the value read from the register Raises @@ -702,7 +702,7 @@ def read_u8(self, address: int) -> ReplyHarpMessage | None: return reply - def read_s8(self, address: int) -> ReplyHarpMessage | None: + def read_s8(self, address: int) -> HarpMessage | None: """ Reads the value of a register of type S8. @@ -713,7 +713,7 @@ def read_s8(self, address: int) -> ReplyHarpMessage | None: Returns ------- - ReplyHarpMessage + HarpMessage The reply to the Harp message that will contain the value read from the register Raises @@ -725,7 +725,7 @@ def read_s8(self, address: int) -> ReplyHarpMessage | None: return reply - def read_u16(self, address: int) -> ReplyHarpMessage | None: + def read_u16(self, address: int) -> HarpMessage | None: """ Reads the value of a register of type U16. @@ -736,7 +736,7 @@ def read_u16(self, address: int) -> ReplyHarpMessage | None: Returns ------- - ReplyHarpMessage + HarpMessage The reply to the Harp message that will contain the value read from the register Raises @@ -748,7 +748,7 @@ def read_u16(self, address: int) -> ReplyHarpMessage | None: return reply - def read_s16(self, address: int) -> ReplyHarpMessage | None: + def read_s16(self, address: int) -> HarpMessage | None: """ Reads the value of a register of type S16. @@ -759,7 +759,7 @@ def read_s16(self, address: int) -> ReplyHarpMessage | None: Returns ------- - ReplyHarpMessage + HarpMessage The reply to the Harp message that will contain the value read from the register Raises @@ -771,7 +771,7 @@ def read_s16(self, address: int) -> ReplyHarpMessage | None: return reply - def read_u32(self, address: int) -> ReplyHarpMessage | None: + def read_u32(self, address: int) -> HarpMessage | None: """ Reads the value of a register of type U32. @@ -782,7 +782,7 @@ def read_u32(self, address: int) -> ReplyHarpMessage | None: Returns ------- - ReplyHarpMessage + HarpMessage The reply to the Harp message that will contain the value read from the register Raises @@ -794,7 +794,7 @@ def read_u32(self, address: int) -> ReplyHarpMessage | None: return reply - def read_s32(self, address: int) -> ReplyHarpMessage | None: + def read_s32(self, address: int) -> HarpMessage | None: """ Reads the value of a register of type S32. @@ -805,7 +805,7 @@ def read_s32(self, address: int) -> ReplyHarpMessage | None: Returns ------- - ReplyHarpMessage + HarpMessage The reply to the Harp message that will contain the value read from the register Raises @@ -817,7 +817,7 @@ def read_s32(self, address: int) -> ReplyHarpMessage | None: return reply - def read_u64(self, address: int) -> ReplyHarpMessage | None: + def read_u64(self, address: int) -> HarpMessage | None: """ Reads the value of a register of type U64. @@ -828,7 +828,7 @@ def read_u64(self, address: int) -> ReplyHarpMessage | None: Returns ------- - ReplyHarpMessage + HarpMessage The reply to the Harp message that will contain the value read from the register Raises @@ -840,7 +840,7 @@ def read_u64(self, address: int) -> ReplyHarpMessage | None: return reply - def read_s64(self, address: int) -> ReplyHarpMessage | None: + def read_s64(self, address: int) -> HarpMessage | None: """ Reads the value of a register of type S64. @@ -851,7 +851,7 @@ def read_s64(self, address: int) -> ReplyHarpMessage | None: Returns ------- - ReplyHarpMessage + HarpMessage The reply to the Harp message that will contain the value read from the register Raises @@ -863,7 +863,7 @@ def read_s64(self, address: int) -> ReplyHarpMessage | None: return reply - def read_float(self, address: int) -> ReplyHarpMessage | None: + def read_float(self, address: int) -> HarpMessage | None: """ Reads the value of a register of type Float. @@ -874,7 +874,7 @@ def read_float(self, address: int) -> ReplyHarpMessage | None: Returns ------- - ReplyHarpMessage + HarpMessage The reply to the Harp message that will contain the value read from the register Raises @@ -886,7 +886,7 @@ def read_float(self, address: int) -> ReplyHarpMessage | None: return reply - def write_u8(self, address: int, value: int | list[int]) -> ReplyHarpMessage | None: + def write_u8(self, address: int, value: int | list[int]) -> HarpMessage | None: """ Writes the value of a register of type U8. @@ -899,7 +899,7 @@ def write_u8(self, address: int, value: int | list[int]) -> ReplyHarpMessage | N Returns ------- - ReplyHarpMessage + HarpMessage The reply to the Harp message Raises @@ -913,7 +913,7 @@ def write_u8(self, address: int, value: int | list[int]) -> ReplyHarpMessage | N return reply - def write_s8(self, address: int, value: int | list[int]) -> ReplyHarpMessage | None: + def write_s8(self, address: int, value: int | list[int]) -> HarpMessage | None: """ Writes the value of a register of type S8. @@ -926,7 +926,7 @@ def write_s8(self, address: int, value: int | list[int]) -> ReplyHarpMessage | N Returns ------- - ReplyHarpMessage + HarpMessage The reply to the Harp message Raises @@ -940,9 +940,7 @@ def write_s8(self, address: int, value: int | list[int]) -> ReplyHarpMessage | N return reply - def write_u16( - self, address: int, value: int | list[int] - ) -> ReplyHarpMessage | None: + def write_u16(self, address: int, value: int | list[int]) -> HarpMessage | None: """ Writes the value of a register of type U16. @@ -955,7 +953,7 @@ def write_u16( Returns ------- - ReplyHarpMessage + HarpMessage The reply to the Harp message Raises @@ -969,9 +967,7 @@ def write_u16( return reply - def write_s16( - self, address: int, value: int | list[int] - ) -> ReplyHarpMessage | None: + def write_s16(self, address: int, value: int | list[int]) -> HarpMessage | None: """ Writes the value of a register of type S16. @@ -984,7 +980,7 @@ def write_s16( Returns ------- - ReplyHarpMessage + HarpMessage The reply to the Harp message Raises @@ -998,9 +994,7 @@ def write_s16( return reply - def write_u32( - self, address: int, value: int | list[int] - ) -> ReplyHarpMessage | None: + def write_u32(self, address: int, value: int | list[int]) -> HarpMessage | None: """ Writes the value of a register of type U32. @@ -1013,7 +1007,7 @@ def write_u32( Returns ------- - ReplyHarpMessage + HarpMessage The reply to the Harp message Raises @@ -1027,9 +1021,7 @@ def write_u32( return reply - def write_s32( - self, address: int, value: int | list[int] - ) -> ReplyHarpMessage | None: + def write_s32(self, address: int, value: int | list[int]) -> HarpMessage | None: """ Writes the value of a register of type S32. @@ -1042,7 +1034,7 @@ def write_s32( Returns ------- - ReplyHarpMessage + HarpMessage The reply to the Harp message Raises @@ -1056,9 +1048,7 @@ def write_s32( return reply - def write_u64( - self, address: int, value: int | list[int] - ) -> ReplyHarpMessage | None: + def write_u64(self, address: int, value: int | list[int]) -> HarpMessage | None: """ Writes the value of a register of type U64. @@ -1071,7 +1061,7 @@ def write_u64( Returns ------- - ReplyHarpMessage + HarpMessage The reply to the Harp message Raises @@ -1085,9 +1075,7 @@ def write_u64( return reply - def write_s64( - self, address: int, value: int | list[int] - ) -> ReplyHarpMessage | None: + def write_s64(self, address: int, value: int | list[int]) -> HarpMessage | None: """ Writes the value of a register of type S64. @@ -1100,7 +1088,7 @@ def write_s64( Returns ------- - ReplyHarpMessage + HarpMessage The reply to the Harp message Raises @@ -1116,7 +1104,7 @@ def write_s64( def write_float( self, address: int, value: float | list[float] - ) -> ReplyHarpMessage | None: + ) -> HarpMessage | None: """ Writes the value of a register of type Float. @@ -1129,7 +1117,7 @@ def write_float( Returns ------- - ReplyHarpMessage + HarpMessage The reply to the Harp message Raises diff --git a/tests/test_device.py b/tests/test_device.py index 4fe552f..106231b 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -2,7 +2,7 @@ # from pyharp import MessageType, PayloadType # from pyharp.device import Device -# from pyharp.messages import HarpMessage, ReplyHarpMessage +# from pyharp.messages import HarpMessage # DEFAULT_ADDRESS = 42 @@ -24,7 +24,7 @@ # register: int = 38 # read_size: int = 35 # TODO: automatically calculate this! -# reply: ReplyHarpMessage = device.send( +# reply: HarpMessage = device.send( # HarpMessage(MessageType.READ, PayloadType.U8, register) # ) # assert reply is not None diff --git a/tests/test_messages.py b/tests/test_messages.py index ed4e8bc..c098526 100644 --- a/tests/test_messages.py +++ b/tests/test_messages.py @@ -2,10 +2,7 @@ from harp.protocol import CommonRegisters, MessageType, PayloadType from harp.protocol.exceptions import HarpException, HarpReadException -from harp.protocol.messages import ( - HarpMessage, - ReplyHarpMessage, -) +from harp.protocol.messages import HarpMessage DEFAULT_ADDRESS = 42 @@ -43,7 +40,7 @@ def test_create_error_cases(): def test_reply_is_error(): - """Test ReplyHarpMessage.is_error property.""" + """Test HarpMessage.is_error property.""" # Create a READ_ERROR message frame = bytearray( [ @@ -67,7 +64,7 @@ def test_reply_is_error(): checksum = sum(frame[:-1]) & 255 frame[-1] = checksum - reply = ReplyHarpMessage(frame) + reply = HarpMessage.parse(frame) assert reply.is_error # Create a normal READ message @@ -93,7 +90,7 @@ def test_reply_is_error(): checksum = sum(frame[:-1]) & 255 frame[-1] = checksum - reply = ReplyHarpMessage(frame) + reply = HarpMessage.parse(frame) assert not reply.is_error @@ -393,7 +390,7 @@ def test_reply_message_str_repr() -> None: checksum = sum(frame[:-1]) & 255 frame[-1] = checksum - reply = ReplyHarpMessage(frame) + reply = HarpMessage.parse(frame) str_repr = str(reply) repr_str = repr(reply) @@ -406,7 +403,7 @@ def test_reply_message_str_repr() -> None: def test_payload_as_string() -> None: - """Test ReplyHarpMessage.payload_as_string().""" + """Test HarpMessage.payload_as_string().""" test_string = "Hello" encoded = test_string.encode("utf-8") @@ -435,7 +432,7 @@ def test_payload_as_string() -> None: checksum = sum(frame[:-1]) & 255 frame[-1] = checksum - reply = ReplyHarpMessage(frame) + reply = HarpMessage.parse(frame) assert reply.payload_as_string() == test_string @@ -464,14 +461,13 @@ def test_harp_message_parse() -> None: frame[-1] = checksum message = HarpMessage.parse(frame) - assert isinstance(message, ReplyHarpMessage) assert message.message_type == MessageType.READ assert message.address == 42 assert message.payload == 123 def test_timestamp_handling() -> None: - """Test timestamp handling in ReplyHarpMessage.""" + """Test timestamp handling in HarpMessage.""" # Create a timestamped message frame = bytearray( [ @@ -495,21 +491,42 @@ def test_timestamp_handling() -> None: checksum = sum(frame[:-1]) & 255 frame[-1] = checksum - reply = ReplyHarpMessage(frame) + reply = HarpMessage.parse(frame) assert reply.timestamp is not None assert reply.timestamp == 1 + 32 * 32e-6 # 1 second + 1 millisecond -# test ReplyHarpMessage without TimestampedU8 raises HarpReadException -def test_reply_without_timestamp_raises() -> None: - """Test that accessing timestamp in non-timestamped message raises exception.""" +# FIXME: handle this better +def test_calculate_checksum() -> None: + """Test the calculate_checksum method.""" + message = HarpMessage(MessageType.READ, PayloadType.U8, DEFAULT_ADDRESS) + message._frame = bytearray([1, 2, 3, 4, 5]) + + # Sum is 15, checksum is 15 (no overflow) + assert message.calculate_checksum() == 15 + + message._frame = bytearray([200, 100, 50, 20, 10]) + # Sum is 380, checksum is 380 % 256 = 124 + assert message.calculate_checksum() == 124 + + +# create harpMessage test, check _raw_payload and change frame and recheck raw_payload +def test_raw_payload_assignment() -> None: + """Test that _raw_payload is assigned correctly based on payload type.""" + # Create a timestamped message frame frame = bytearray( [ MessageType.READ, - 5, + 11, 42, 255, - PayloadType.U8, # Not a timestamped type + PayloadType.TIMESTAMPED_U8, + 0, + 0, + 0, + 0, # timestamp seconds + 0, + 0, # timestamp micros 123, # payload 0, ] @@ -519,19 +536,25 @@ def test_reply_without_timestamp_raises() -> None: checksum = sum(frame[:-1]) & 255 frame[-1] = checksum - with pytest.raises(HarpReadException) as excinfo: - ReplyHarpMessage(frame) - assert "not a timestamped payload type" in str(excinfo.value) - + message = HarpMessage.parse(frame) + assert message._raw_payload == frame[11:-1] -def test_calculate_checksum() -> None: - """Test the calculate_checksum method.""" - message = HarpMessage(MessageType.READ, PayloadType.U8, DEFAULT_ADDRESS) - message._frame = bytearray([1, 2, 3, 4, 5]) + # Create a non-timestamped message frame + frame_no_timestamp = bytearray( + [ + MessageType.READ, + 5, + 42, + 255, + PayloadType.U8, + 123, # payload + 0, + ] + ) # checksum placeholder - # Sum is 15, checksum is 15 (no overflow) - assert message.calculate_checksum() == 15 + # Fix checksum + checksum = sum(frame_no_timestamp[:-1]) & 255 + frame_no_timestamp[-1] = checksum - message._frame = bytearray([200, 100, 50, 20, 10]) - # Sum is 380, checksum is 380 % 256 = 124 - assert message.calculate_checksum() == 124 + message_no_timestamp = HarpMessage.parse(frame_no_timestamp) + assert message_no_timestamp._raw_payload == frame_no_timestamp[5:-1] From 6f97c9248c68c81141201aa3ef5b90dc43648785 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Fri, 17 Oct 2025 16:23:44 +0100 Subject: [PATCH 167/267] Refactor timeout variables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- src/harp-protocol/harp/protocol/exceptions.py | 14 +++++++++++--- src/harp-serial/harp/serial/device.py | 16 +++++++--------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/harp-protocol/harp/protocol/exceptions.py b/src/harp-protocol/harp/protocol/exceptions.py index 61650af..3e2ee73 100644 --- a/src/harp-protocol/harp/protocol/exceptions.py +++ b/src/harp-protocol/harp/protocol/exceptions.py @@ -27,6 +27,14 @@ def __init__(self, register): class HarpTimeoutError(HarpException): """Raised when no reply is received within the configured timeout.""" - def __init__(self, timeout_s: float): - super().__init__(f"No reply received within {timeout_s} seconds.") - self.timeout_s = timeout_s + def __init__(self, timeout: float): + """ + Creates a new HarpTimeoutError with the given timeout. + + Parameters + ---------- + timeout: float + Number of seconds waited before the timeout occurred. + """ + super().__init__(f"No reply received within {timeout} seconds.") + self.timeout = timeout diff --git a/src/harp-serial/harp/serial/device.py b/src/harp-serial/harp/serial/device.py index a651bca..73e4778 100644 --- a/src/harp-serial/harp/serial/device.py +++ b/src/harp-serial/harp/serial/device.py @@ -92,15 +92,13 @@ class Device: _ser: HarpSerial _dump_file_path: Optional[Path] _dump_file: Optional[BufferedWriter] = None - _read_timeout_s: float - - _TIMEOUT_S: float = 1.0 + _timeout: float def __init__( self, serial_port: str, dump_file_path: Optional[str] = None, - read_timeout_s: float = 1, + timeout: float = 1, timeout_strategy: TimeoutStrategy = TimeoutStrategy.RAISE, ): """ @@ -110,7 +108,7 @@ def __init__( The serial port used to establish the connection with the Harp device. It must be denoted as `/dev/ttyUSBx` in Linux and `COMx` in Windows, where `x` is the number of the serial port dump_file_path: str, optional The binary file to which all Harp messages will be written - read_timeout_s: float, optional + timeout: float, optional The timeout in seconds when waiting for a reply from the device timeout_strategy: TimeoutStrategy, optional The strategy to handle timeouts when waiting for a reply from the device @@ -120,7 +118,7 @@ def __init__( self._dump_file_path = None if dump_file_path is not None: self._dump_file_path = Path() / dump_file_path - self._read_timeout_s = read_timeout_s + self._timeout = timeout self._timeout_strategy = timeout_strategy # Connect to the Harp device and load the data stored in the device's common registers @@ -168,7 +166,7 @@ def connect(self) -> None: self._ser = HarpSerial( self._serial_port, # "/dev/tty.usbserial-A106C8O9" baudrate=1000000, - timeout=self._TIMEOUT_S, + timeout=self._timeout, parity=serial.PARITY_NONE, stopbits=1, bytesize=8, @@ -612,7 +610,7 @@ def send( try: reply = self._read() except TimeoutError: - hte = HarpTimeoutError(self._read_timeout_s) + hte = HarpTimeoutError(self._timeout) if strategy in ( TimeoutStrategy.LOG_AND_RAISE, TimeoutStrategy.LOG_AND_NONE, @@ -640,7 +638,7 @@ def _read(self) -> HarpMessage: If no reply is received within the timeout period """ try: - return self._ser.msg_q.get(block=True, timeout=self._read_timeout_s) + return self._ser.msg_q.get(block=True, timeout=self._timeout) except queue.Empty: raise TimeoutError("No reply received within the timeout period.") From 1f241907f712794248f1c6beaf126f2d6336d4a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Fri, 17 Oct 2025 16:38:44 +0100 Subject: [PATCH 168/267] Rename HarpTimeoutError to HarpTimeoutException and add context to it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- src/harp-protocol/harp/protocol/exceptions.py | 25 ++++++++-- src/harp-protocol/harp/protocol/messages.py | 4 +- src/harp-serial/harp/serial/device.py | 48 +++++++++---------- 3 files changed, 47 insertions(+), 30 deletions(-) diff --git a/src/harp-protocol/harp/protocol/exceptions.py b/src/harp-protocol/harp/protocol/exceptions.py index 3e2ee73..1678147 100644 --- a/src/harp-protocol/harp/protocol/exceptions.py +++ b/src/harp-protocol/harp/protocol/exceptions.py @@ -1,3 +1,8 @@ +from typing import Optional + +from harp.protocol.messages import HarpMessage + + class HarpException(Exception): """Base class for all exceptions raised related with Harp.""" @@ -24,17 +29,27 @@ def __init__(self, register): self.register = register -class HarpTimeoutError(HarpException): +class HarpTimeoutException(HarpException): """Raised when no reply is received within the configured timeout.""" - def __init__(self, timeout: float): + def __init__(self, timeout: float, message: Optional[HarpMessage] = None): """ - Creates a new HarpTimeoutError with the given timeout. + Creates a new HarpTimeoutException with the given timeout. Parameters ---------- timeout: float - Number of seconds waited before the timeout occurred. + The timeout duration in seconds. + message: HarpMessage, optional + The Harp message that was sent when the timeout occurred. """ - super().__init__(f"No reply received within {timeout} seconds.") + if message is None: + error_msg = f"No reply received within {timeout} seconds." + else: + error_msg = ( + f"No reply received within {timeout} seconds for message:\r\n{message}" + ) + + super().__init__(error_msg) self.timeout = timeout + self.message = message diff --git a/src/harp-protocol/harp/protocol/messages.py b/src/harp-protocol/harp/protocol/messages.py index a253522..f765ab8 100644 --- a/src/harp-protocol/harp/protocol/messages.py +++ b/src/harp-protocol/harp/protocol/messages.py @@ -4,7 +4,6 @@ from typing import Optional, Union from harp.protocol import MessageType, PayloadType -from harp.protocol.exceptions import HarpException, HarpReadException class HarpMessage: @@ -56,6 +55,9 @@ def __init__( The payload of the message. If message_type == MessageType.WRITE, the value cannot be None """ if message_type in [MessageType.WRITE, MessageType.EVENT] and value is None: + # prevents circular import + from harp.protocol.exceptions import HarpException + raise HarpException( "The value cannot be None if the message type is equal to MessageType.WRITE!" ) diff --git a/src/harp-serial/harp/serial/device.py b/src/harp-serial/harp/serial/device.py index 73e4778..cad207d 100644 --- a/src/harp-serial/harp/serial/device.py +++ b/src/harp-serial/harp/serial/device.py @@ -18,7 +18,7 @@ ResetMode, ) from harp.protocol.device_names import device_names -from harp.protocol.exceptions import HarpTimeoutError +from harp.protocol.exceptions import HarpTimeoutException from harp.protocol.messages import HarpMessage from harp.serial.harp_serial import HarpSerial @@ -30,16 +30,16 @@ class TimeoutStrategy(Enum): Attributes ---------- RAISE : str - Raise HarpTimeoutError + Raise HarpTimeoutException RETURN_NONE : str Return None LOG_AND_RAISE : str - Log the timeout and raise HarpTimeoutError + Log the timeout and raise HarpTimeoutException LOG_AND_NONE : str Log the timeout and return None """ - RAISE = "raise" # Raise HarpTimeoutError + RAISE = "raise" # Raise HarpTimeoutException RETURN_NONE = "return_none" # Return None LOG_AND_RAISE = "log_and_raise" LOG_AND_NONE = "log_and_none" @@ -597,7 +597,7 @@ def send( Raises ------- - HarpTimeoutError + HarpTimeoutException If no reply is received and the effective strategy requires raising """ self._ser.write(message.frame) @@ -610,7 +610,7 @@ def send( try: reply = self._read() except TimeoutError: - hte = HarpTimeoutError(self._timeout) + hte = HarpTimeoutException(self._timeout, message) if strategy in ( TimeoutStrategy.LOG_AND_RAISE, TimeoutStrategy.LOG_AND_NONE, @@ -693,7 +693,7 @@ def read_u8(self, address: int) -> HarpMessage | None: Raises ------ - HarpTimeoutError + HarpTimeoutException If no reply is received and the effective strategy requires raising """ reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) @@ -716,7 +716,7 @@ def read_s8(self, address: int) -> HarpMessage | None: Raises ------ - HarpTimeoutError + HarpTimeoutException If no reply is received and the effective strategy requires raising """ reply = self.send(HarpMessage(MessageType.READ, PayloadType.S8, address)) @@ -739,7 +739,7 @@ def read_u16(self, address: int) -> HarpMessage | None: Raises ------ - HarpTimeoutError + HarpTimeoutException If no reply is received and the effective strategy requires raising """ reply = self.send(HarpMessage(MessageType.READ, PayloadType.U16, address)) @@ -762,7 +762,7 @@ def read_s16(self, address: int) -> HarpMessage | None: Raises ------ - HarpTimeoutError + HarpTimeoutException If no reply is received and the effective strategy requires raising """ reply = self.send(HarpMessage(MessageType.READ, PayloadType.S16, address)) @@ -785,7 +785,7 @@ def read_u32(self, address: int) -> HarpMessage | None: Raises ------ - HarpTimeoutError + HarpTimeoutException If no reply is received and the effective strategy requires raising """ reply = self.send(HarpMessage(MessageType.READ, PayloadType.U32, address)) @@ -808,7 +808,7 @@ def read_s32(self, address: int) -> HarpMessage | None: Raises ------ - HarpTimeoutError + HarpTimeoutException If no reply is received and the effective strategy requires raising """ reply = self.send(HarpMessage(MessageType.READ, PayloadType.S32, address)) @@ -831,7 +831,7 @@ def read_u64(self, address: int) -> HarpMessage | None: Raises ------ - HarpTimeoutError + HarpTimeoutException If no reply is received and the effective strategy requires raising """ reply = self.send(HarpMessage(MessageType.READ, PayloadType.U64, address)) @@ -854,7 +854,7 @@ def read_s64(self, address: int) -> HarpMessage | None: Raises ------ - HarpTimeoutError + HarpTimeoutException If no reply is received and the effective strategy requires raising """ reply = self.send(HarpMessage(MessageType.READ, PayloadType.S64, address)) @@ -877,7 +877,7 @@ def read_float(self, address: int) -> HarpMessage | None: Raises ------ - HarpTimeoutError + HarpTimeoutException If no reply is received and the effective strategy requires raising """ reply = self.send(HarpMessage(MessageType.READ, PayloadType.FLOAT, address)) @@ -902,7 +902,7 @@ def write_u8(self, address: int, value: int | list[int]) -> HarpMessage | None: Raises ------ - HarpTimeoutError + HarpTimeoutException If no reply is received and the effective strategy requires raising """ reply = self.send( @@ -929,7 +929,7 @@ def write_s8(self, address: int, value: int | list[int]) -> HarpMessage | None: Raises ------ - HarpTimeoutError + HarpTimeoutException If no reply is received and the effective strategy requires raising """ reply = self.send( @@ -956,7 +956,7 @@ def write_u16(self, address: int, value: int | list[int]) -> HarpMessage | None: Raises ------ - HarpTimeoutError + HarpTimeoutException If no reply is received and the effective strategy requires raising """ reply = self.send( @@ -983,7 +983,7 @@ def write_s16(self, address: int, value: int | list[int]) -> HarpMessage | None: Raises ------ - HarpTimeoutError + HarpTimeoutException If no reply is received and the effective strategy requires raising """ reply = self.send( @@ -1010,7 +1010,7 @@ def write_u32(self, address: int, value: int | list[int]) -> HarpMessage | None: Raises ------ - HarpTimeoutError + HarpTimeoutException If no reply is received and the effective strategy requires raising """ reply = self.send( @@ -1037,7 +1037,7 @@ def write_s32(self, address: int, value: int | list[int]) -> HarpMessage | None: Raises ------ - HarpTimeoutError + HarpTimeoutException If no reply is received and the effective strategy requires raising """ reply = self.send( @@ -1064,7 +1064,7 @@ def write_u64(self, address: int, value: int | list[int]) -> HarpMessage | None: Raises ------ - HarpTimeoutError + HarpTimeoutException If no reply is received and the effective strategy requires raising """ reply = self.send( @@ -1091,7 +1091,7 @@ def write_s64(self, address: int, value: int | list[int]) -> HarpMessage | None: Raises ------ - HarpTimeoutError + HarpTimeoutException If no reply is received and the effective strategy requires raising """ reply = self.send( @@ -1120,7 +1120,7 @@ def write_float( Raises ------ - HarpTimeoutError + HarpTimeoutException If no reply is received and the effective strategy requires raising """ reply = self.send( From 36a83636d83a329780f4d531edaa0841a3f49606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Fri, 17 Oct 2025 16:50:40 +0100 Subject: [PATCH 169/267] Update comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- src/harp-serial/harp/serial/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/harp-serial/harp/serial/device.py b/src/harp-serial/harp/serial/device.py index cad207d..f7850bc 100644 --- a/src/harp-serial/harp/serial/device.py +++ b/src/harp-serial/harp/serial/device.py @@ -1,4 +1,4 @@ -from __future__ import annotations # enable subscriptable type hints for lists. +from __future__ import annotations # for type hints (PEP 563) import logging import queue From e97480cb34d0faab667c5e678fb488fe350ef92f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 22 Oct 2025 10:31:10 +0100 Subject: [PATCH 170/267] Add HarpMessage context to Harp exceptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- src/harp-protocol/harp/protocol/exceptions.py | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/src/harp-protocol/harp/protocol/exceptions.py b/src/harp-protocol/harp/protocol/exceptions.py index 1678147..dc095a0 100644 --- a/src/harp-protocol/harp/protocol/exceptions.py +++ b/src/harp-protocol/harp/protocol/exceptions.py @@ -6,7 +6,9 @@ class HarpException(Exception): """Base class for all exceptions raised related with Harp.""" - pass + def __init__(self, error_msg: str, message: Optional[HarpMessage] = None): + super().__init__(error_msg) + self.message = message class HarpWriteException(HarpException): @@ -14,9 +16,8 @@ class HarpWriteException(HarpException): Exception raised when there is an error writing to a register in the Harp device. """ - def __init__(self, register): - super().__init__(f"Error writing to register {register}") - self.register = register + def __init__(self, register_str: str, message: HarpMessage): + super().__init__(f"Error writing to device on address {register_str}.", message) class HarpReadException(HarpException): @@ -24,15 +25,14 @@ class HarpReadException(HarpException): Exception raised when there is an error reading from a register in the Harp device. """ - def __init__(self, register): - super().__init__(f"Error reading from register {register}") - self.register = register + def __init__(self, register_str: str, message: HarpMessage): + super().__init__(f'Error reading from register "{register_str}".', message) class HarpTimeoutException(HarpException): """Raised when no reply is received within the configured timeout.""" - def __init__(self, timeout: float, message: Optional[HarpMessage] = None): + def __init__(self, timeout: float, message: HarpMessage): """ Creates a new HarpTimeoutException with the given timeout. @@ -40,16 +40,11 @@ def __init__(self, timeout: float, message: Optional[HarpMessage] = None): ---------- timeout: float The timeout duration in seconds. - message: HarpMessage, optional + message: HarpMessage The Harp message that was sent when the timeout occurred. """ - if message is None: - error_msg = f"No reply received within {timeout} seconds." - else: - error_msg = ( - f"No reply received within {timeout} seconds for message:\r\n{message}" - ) - - super().__init__(error_msg) + error_msg = ( + f"No reply received within {timeout} seconds for message:\r\n{message}" + ) + super().__init__(error_msg, message) self.timeout = timeout - self.message = message From 8e84b3af7bdcfe9bcb0f5dbd7d94adc9cad66633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 22 Oct 2025 11:35:21 +0100 Subject: [PATCH 171/267] Remove constraint when creating HarpMessage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- src/harp-protocol/harp/protocol/messages.py | 8 -------- tests/test_messages.py | 8 -------- 2 files changed, 16 deletions(-) diff --git a/src/harp-protocol/harp/protocol/messages.py b/src/harp-protocol/harp/protocol/messages.py index f765ab8..58e9c0c 100644 --- a/src/harp-protocol/harp/protocol/messages.py +++ b/src/harp-protocol/harp/protocol/messages.py @@ -54,14 +54,6 @@ def __init__( value: int | list[int] | float | list[float], optional The payload of the message. If message_type == MessageType.WRITE, the value cannot be None """ - if message_type in [MessageType.WRITE, MessageType.EVENT] and value is None: - # prevents circular import - from harp.protocol.exceptions import HarpException - - raise HarpException( - "The value cannot be None if the message type is equal to MessageType.WRITE!" - ) - self._frame = bytearray() payload = bytearray() diff --git a/tests/test_messages.py b/tests/test_messages.py index c098526..c9a167c 100644 --- a/tests/test_messages.py +++ b/tests/test_messages.py @@ -31,14 +31,6 @@ def test_create_write_list(): assert list(payload_bytes) == values -def test_create_error_cases(): - """Test error cases while creating HarpMessage.""" - # Test WRITE with None value - with pytest.raises(HarpException) as excinfo: - HarpMessage(MessageType.WRITE, PayloadType.U8, 42, None) - assert "value cannot be None" in str(excinfo.value) - - def test_reply_is_error(): """Test HarpMessage.is_error property.""" # Create a READ_ERROR message From aa564c7eb15e20e42a6e2734930ca370e7212a13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 22 Oct 2025 11:48:36 +0100 Subject: [PATCH 172/267] Change build system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- src/harp-protocol/pyproject.toml | 12 +++++------- src/harp-serial/pyproject.toml | 12 +++++------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/harp-protocol/pyproject.toml b/src/harp-protocol/pyproject.toml index 7bffea0..0c0d2a0 100644 --- a/src/harp-protocol/pyproject.toml +++ b/src/harp-protocol/pyproject.toml @@ -8,11 +8,9 @@ keywords = ['python', 'harp'] requires-python = ">=3.9,<4.0" [build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" +requires = ["uv_build>=0.9.5,<0.10.0"] +build-backend = "uv_build" -[tool.hatch.build.targets.wheel] -include = [ - "harp", - "harp/**/*", -] +[tool.uv.build-backend] +module-name = "harp.protocol" +module-root = "" diff --git a/src/harp-serial/pyproject.toml b/src/harp-serial/pyproject.toml index cd4421b..35de1f7 100644 --- a/src/harp-serial/pyproject.toml +++ b/src/harp-serial/pyproject.toml @@ -15,11 +15,9 @@ dependencies = [ harp-protocol = { workspace = true } [build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" +requires = ["uv_build>=0.9.5,<0.10.0"] +build-backend = "uv_build" -[tool.hatch.build.targets.wheel] -include = [ - "harp", - "harp/**/*", -] +[tool.uv.build-backend] +module-name = "harp.serial" +module-root = "" From 3d8d10d69430a2f6b3b64e862304c1688f291e36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 22 Oct 2025 14:20:21 +0100 Subject: [PATCH 173/267] Remove device_names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- .../harp/protocol/device_names.py | 52 ------------------- src/harp-serial/harp/serial/device.py | 20 +------ 2 files changed, 2 insertions(+), 70 deletions(-) delete mode 100644 src/harp-protocol/harp/protocol/device_names.py diff --git a/src/harp-protocol/harp/protocol/device_names.py b/src/harp-protocol/harp/protocol/device_names.py deleted file mode 100644 index 4682267..0000000 --- a/src/harp-protocol/harp/protocol/device_names.py +++ /dev/null @@ -1,52 +0,0 @@ -from collections import defaultdict - -# This file contains the device names for the current version of the harp library. -# These names were extracted from https://github.com/harp-tech/protocol/blob/main/whoami.yml -# commit used: https://github.com/harp-tech/protocol/commit/3e2a228 - -current_device_names = { - 256: "USBHub", - 1024: "Poke", - 1040: "MultiPwmGenerator", - 1056: "Wear", - 1058: "WearBaseStationGen2", - 1072: "Driver12Volts", - 1088: "LedController", - 1104: "Synchronizer", - 1106: "InputExpander", - 1108: "OutputExpander", - 1121: "SimpleAnalogGenerator", - 1130: "StepperDriver", - 1136: "Archimedes", - 1140: "Olfactometer", - 1152: "ClockSynchronizer", - 1154: "TimestampGeneratorGen1", - 1158: "TimestampGeneratorGen3", - 1168: "CameraController", - 1170: "CameraControllerGen2", - 1184: "PyControlAdapter", - 1200: "FlyPad", - 1216: "Behavior", - 1224: "VestibularH1", - 1225: "VestibularH2", - 1232: "LoadCells", - 1236: "AnalogInput", - 1248: "RgbArray", - 1280: "SoundCard", - 1282: "CurrentDriver", - 1296: "SyringePump", - 1298: "LaserDriverController", - 1400: "LicketySplit", - 1401: "SniffDetector", - 1402: "Treadmill", - 1403: "cuTTLefish", - 1404: "WhiteRabbit", - 1405: "EnvironmentSensor", - 2064: "NeurophotometricsFP3002", - 2080: "Ibl_behavior_control", - 2094: "RfidReader", - 2110: "Pluma", -} - -device_names = defaultdict(lambda: "NotSpecified") -device_names.update(current_device_names) diff --git a/src/harp-serial/harp/serial/device.py b/src/harp-serial/harp/serial/device.py index f7850bc..4d387d1 100644 --- a/src/harp-serial/harp/serial/device.py +++ b/src/harp-serial/harp/serial/device.py @@ -17,7 +17,6 @@ PayloadType, ResetMode, ) -from harp.protocol.device_names import device_names from harp.protocol.exceptions import HarpTimeoutException from harp.protocol.messages import HarpMessage from harp.serial.harp_serial import HarpSerial @@ -53,8 +52,6 @@ class Device: ---------- WHO_AM_I : int The device ID number. A list of devices can be found [here](https://github.com/harp-tech/protocol/blob/main/whoami.md) - DEFAULT_DEVICE_NAME : str - The device name, i.e. "Behavior". This name is derived by cross-referencing the `WHO_AM_I` identifier with the corresponding device name in the `device_names` dictionary HW_VERSION_H : int The major hardware version HW_VERSION_L : int @@ -76,7 +73,6 @@ class Device: """ WHO_AM_I: int - DEFAULT_DEVICE_NAME: str HW_VERSION_H: int HW_VERSION_L: int ASSEMBLY_VERSION: int @@ -130,7 +126,6 @@ def load(self) -> None: Loads the data stored in the device's common registers. """ self.WHO_AM_I = self._read_who_am_i() - self.DEFAULT_DEVICE_NAME = self._read_default_device_name() self.HW_VERSION_H = self._read_hw_version_h() self.HW_VERSION_L = self._read_hw_version_l() self.ASSEMBLY_VERSION = self._read_assembly_version() @@ -148,14 +143,14 @@ def info(self) -> None: Prints the device information. """ print("Device info:") - print(f"* Who am I: ({self.WHO_AM_I}) {self.DEFAULT_DEVICE_NAME}") + print(f"* Who am I: ({self.WHO_AM_I})") print(f"* HW version: {self.HW_VERSION_H}.{self.HW_VERSION_L}") print(f"* Assembly version: {self.ASSEMBLY_VERSION}") print(f"* HARP version: {self.CORE_VERSION_H}.{self.CORE_VERSION_L}") print( f"* Firmware version: {self.FIRMWARE_VERSION_H}.{self.FIRMWARE_VERSION_L}" ) - print(f"* Device user name: {self.DEVICE_NAME}") + print(f"* Device name: {self.DEVICE_NAME}") print(f"* Serial number: {self.SERIAL_NUMBER}") print(f"* Mode: {self._read_device_mode().name}") @@ -1144,17 +1139,6 @@ def _read_who_am_i(self) -> int: return reply.payload - def _read_default_device_name(self) -> str: - """ - Returns the `DEFAULT_DEVICE_NAME` by cross-referencing the `WHO_AM_I` with the corresponding device name in the `device_names` dictionary. - - Returns - ------- - str - The default device name - """ - return device_names.get(self.WHO_AM_I, "Unknown device") - def _read_hw_version_h(self) -> int: """ Reads the value stored in the `HW_VERSION_H` register. From ccdef2a4bad24f668caee35c26d45be67b0bbb85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 22 Oct 2025 15:25:24 +0100 Subject: [PATCH 174/267] Handle potential serial connection errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- src/harp-serial/harp/serial/device.py | 14 +++++++++++--- src/harp-serial/harp/serial/harp_serial.py | 11 +++++++++-- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/harp-serial/harp/serial/device.py b/src/harp-serial/harp/serial/device.py index 4d387d1..3241a08 100644 --- a/src/harp-serial/harp/serial/device.py +++ b/src/harp-serial/harp/serial/device.py @@ -17,7 +17,7 @@ PayloadType, ResetMode, ) -from harp.protocol.exceptions import HarpTimeoutException +from harp.protocol.exceptions import HarpException, HarpTimeoutException from harp.protocol.messages import HarpMessage from harp.serial.harp_serial import HarpSerial @@ -159,7 +159,7 @@ def connect(self) -> None: Connects to the Harp device. """ self._ser = HarpSerial( - self._serial_port, # "/dev/tty.usbserial-A106C8O9" + self._serial_port, baudrate=1000000, timeout=self._timeout, parity=serial.PARITY_NONE, @@ -1135,7 +1135,15 @@ def _read_who_am_i(self) -> int: """ address = CommonRegisters.WHO_AM_I - reply = self.send(HarpMessage(MessageType.READ, PayloadType.U16, address)) + # Attempt to read the WHO_AM_I register to verify if the device is a Harp device + error_msg = "This is not a Harp device." + try: + reply = self.send(HarpMessage(MessageType.READ, PayloadType.U16, address)) + except HarpTimeoutException: + raise HarpException(error_msg) + + if reply is None: + raise HarpException(error_msg) return reply.payload diff --git a/src/harp-serial/harp/serial/harp_serial.py b/src/harp-serial/harp/serial/harp_serial.py index c966d79..00c9593 100644 --- a/src/harp-serial/harp/serial/harp_serial.py +++ b/src/harp-serial/harp/serial/harp_serial.py @@ -6,7 +6,7 @@ import serial import serial.threaded - +from harp.protocol.exceptions import HarpException from harp.protocol.messages import HarpMessage, MessageType @@ -98,8 +98,15 @@ def __init__(self, serial_port: str, **kwargs): serial_port : str The serial port used to establish the connection with the Harp device. It must be denoted as `/dev/ttyUSBx` in Linux and `COMx` in Windows, where `x` is the number of the serial port """ + ser_kwargs = dict(kwargs) + ser_kwargs.setdefault("exclusive", True) # Connect to the Harp device - self._ser = serial.Serial(serial_port, **kwargs) + try: + self._ser = serial.Serial(serial_port, **ser_kwargs) + except serial.serialutil.SerialException: + raise HarpException( + f"Error connecting to device. Resource might be busy or without proper permissions: {serial_port}" + ) self.log = logging.getLogger(f"{__name__}.{self.__class__.__name__}") From c24c51fbbb6203440800592679cb6331fdf51a27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 22 Oct 2025 15:39:06 +0100 Subject: [PATCH 175/267] Update minimum Python version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- pyproject.toml | 2 +- src/harp-protocol/pyproject.toml | 2 +- src/harp-serial/pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ae3c412..03bcb08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ authors = [{ name= "Hardware and Software Platform, Champalimaud Foundation", em license = "MIT" readme = 'README.md' keywords = ['python', 'harp'] -requires-python = ">=3.9,<4.0" +requires-python = ">=3.10,<4.0" dependencies = ["harp-protocol", "harp-serial"] [project.urls] diff --git a/src/harp-protocol/pyproject.toml b/src/harp-protocol/pyproject.toml index 0c0d2a0..c0ce0dd 100644 --- a/src/harp-protocol/pyproject.toml +++ b/src/harp-protocol/pyproject.toml @@ -5,7 +5,7 @@ description = "Library with the base types for Harp protocol usage." authors = [{ name= "Hardware and Software Platform, Champalimaud Foundation", email="software@research.fchampalimaud.org"}] license = "MIT" keywords = ['python', 'harp'] -requires-python = ">=3.9,<4.0" +requires-python = ">=3.10,<4.0" [build-system] requires = ["uv_build>=0.9.5,<0.10.0"] diff --git a/src/harp-serial/pyproject.toml b/src/harp-serial/pyproject.toml index 35de1f7..57d7e34 100644 --- a/src/harp-serial/pyproject.toml +++ b/src/harp-serial/pyproject.toml @@ -5,7 +5,7 @@ description = "Library for data acquisition and control of devices implementing authors = [{ name= "Hardware and Software Platform, Champalimaud Foundation", email="software@research.fchampalimaud.org"}] license = "MIT" keywords = ['python', 'harp'] -requires-python = ">=3.9,<4.0" +requires-python = ">=3.10,<4.0" dependencies = [ "harp-protocol==0.3.0", "pyserial>=3.5", From e1025d5835672656bee336f1a1b907bb600e1f44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Grilo?= Date: Thu, 23 Oct 2025 10:10:28 +0100 Subject: [PATCH 176/267] Fix: write events to dump file --- src/harp-serial/harp/serial/device.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/harp-serial/harp/serial/device.py b/src/harp-serial/harp/serial/device.py index f7850bc..4cd6176 100644 --- a/src/harp-serial/harp/serial/device.py +++ b/src/harp-serial/harp/serial/device.py @@ -661,7 +661,9 @@ def get_events(self) -> list[HarpMessage]: msgs = [] while True: try: - msgs.append(self._ser.event_q.get(timeout=False)) + msg = self._ser.event_q.get(timeout=False) + self._dump_reply(msg.frame) + msgs.append(msg) except queue.Empty: break return msgs From 1fbfb2dda2d504acdcca42c6ec0f9be8f5eb0df9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Fri, 24 Oct 2025 14:40:20 +0100 Subject: [PATCH 177/267] Refactor to improve message reading and timeout handling in Device MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- src/harp-serial/harp/serial/device.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/harp-serial/harp/serial/device.py b/src/harp-serial/harp/serial/device.py index 88ada61..3167c36 100644 --- a/src/harp-serial/harp/serial/device.py +++ b/src/harp-serial/harp/serial/device.py @@ -222,10 +222,11 @@ def dump_registers(self) -> list: replies = [] while True: msg = self._read() - if msg is not None: - replies.append(msg) - else: + if msg is None: break + else: + replies.append(msg) + self._dump_reply(msg.frame) return replies def read_operation_ctrl(self): @@ -602,9 +603,8 @@ def send( strategy = timeout_strategy or self._timeout_strategy - try: - reply = self._read() - except TimeoutError: + reply = self._read() + if reply is None: hte = HarpTimeoutException(self._timeout, message) if strategy in ( TimeoutStrategy.LOG_AND_RAISE, @@ -613,18 +613,17 @@ def send( self.log.warning(str(hte)) if strategy in (TimeoutStrategy.RAISE, TimeoutStrategy.LOG_AND_RAISE): raise hte - return None - - self._dump_reply(reply.frame) + else: + self._dump_reply(reply.frame) return reply - def _read(self) -> HarpMessage: + def _read(self) -> HarpMessage | None: """ Reads an incoming serial message in a blocking way. Returns ------- - HarpMessage + HarpMessage | None The incoming Harp message in case it exists Raises @@ -635,7 +634,7 @@ def _read(self) -> HarpMessage: try: return self._ser.msg_q.get(block=True, timeout=self._timeout) except queue.Empty: - raise TimeoutError("No reply received within the timeout period.") + return None def _dump_reply(self, reply: bytearray): """ From 5f6267315f90e922287058fc43c1e2d777b9e632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Fri, 24 Oct 2025 15:08:34 +0100 Subject: [PATCH 178/267] Separate docs from dev dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- pyproject.toml | 10 +++- uv.lock | 125 +++++++------------------------------------------ 2 files changed, 26 insertions(+), 109 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 03bcb08..9810daf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,10 @@ license = "MIT" readme = 'README.md' keywords = ['python', 'harp'] requires-python = ">=3.10,<4.0" -dependencies = ["harp-protocol", "harp-serial"] +dependencies = [ + "harp-protocol", + "harp-serial", +] [project.urls] Repository = "https://github.com/fchampalimaud/pyharp/" @@ -24,7 +27,7 @@ members = [ ] [dependency-groups] -dev = [ +docs = [ "griffe-fieldz>=0.2.1", "mkdocs>=1.6.1", "mkdocs-codeinclude-plugin>=0.2.1", @@ -34,6 +37,9 @@ dev = [ "mkdocs-material>=9.6.9", "mkdocs-monorepo-plugin>=1.1.2", "mkdocstrings-python>=1.16.6", +] + +dev = [ "pytest>=8.3.5", "pytest-cov>=6.1.1", "ruff>=0.11.0", diff --git a/uv.lock b/uv.lock index d3ac2cc..1072820 100644 --- a/uv.lock +++ b/uv.lock @@ -1,9 +1,9 @@ version = 1 revision = 3 -requires-python = ">=3.9, <4.0" +requires-python = ">=3.10, <4.0" resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version < '3.10'", + "python_full_version >= '3.13'", + "python_full_version < '3.13'", ] [manifest] @@ -115,44 +115,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, - { url = "https://files.pythonhosted.org/packages/c2/ca/9a0983dd5c8e9733565cf3db4df2b0a2e9a82659fd8aa2a868ac6e4a991f/charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05", size = 207520, upload-time = "2025-08-09T07:57:11.026Z" }, - { url = "https://files.pythonhosted.org/packages/39/c6/99271dc37243a4f925b09090493fb96c9333d7992c6187f5cfe5312008d2/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e", size = 147307, upload-time = "2025-08-09T07:57:12.4Z" }, - { url = "https://files.pythonhosted.org/packages/e4/69/132eab043356bba06eb333cc2cc60c6340857d0a2e4ca6dc2b51312886b3/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99", size = 160448, upload-time = "2025-08-09T07:57:13.712Z" }, - { url = "https://files.pythonhosted.org/packages/04/9a/914d294daa4809c57667b77470533e65def9c0be1ef8b4c1183a99170e9d/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7", size = 157758, upload-time = "2025-08-09T07:57:14.979Z" }, - { url = "https://files.pythonhosted.org/packages/b0/a8/6f5bcf1bcf63cb45625f7c5cadca026121ff8a6c8a3256d8d8cd59302663/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7", size = 152487, upload-time = "2025-08-09T07:57:16.332Z" }, - { url = "https://files.pythonhosted.org/packages/c4/72/d3d0e9592f4e504f9dea08b8db270821c909558c353dc3b457ed2509f2fb/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19", size = 150054, upload-time = "2025-08-09T07:57:17.576Z" }, - { url = "https://files.pythonhosted.org/packages/20/30/5f64fe3981677fe63fa987b80e6c01042eb5ff653ff7cec1b7bd9268e54e/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312", size = 161703, upload-time = "2025-08-09T07:57:20.012Z" }, - { url = "https://files.pythonhosted.org/packages/e1/ef/dd08b2cac9284fd59e70f7d97382c33a3d0a926e45b15fc21b3308324ffd/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc", size = 159096, upload-time = "2025-08-09T07:57:21.329Z" }, - { url = "https://files.pythonhosted.org/packages/45/8c/dcef87cfc2b3f002a6478f38906f9040302c68aebe21468090e39cde1445/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34", size = 153852, upload-time = "2025-08-09T07:57:22.608Z" }, - { url = "https://files.pythonhosted.org/packages/63/86/9cbd533bd37883d467fcd1bd491b3547a3532d0fbb46de2b99feeebf185e/charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432", size = 99840, upload-time = "2025-08-09T07:57:23.883Z" }, - { url = "https://files.pythonhosted.org/packages/ce/d6/7e805c8e5c46ff9729c49950acc4ee0aeb55efb8b3a56687658ad10c3216/charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca", size = 107438, upload-time = "2025-08-09T07:57:25.287Z" }, { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, ] -[[package]] -name = "click" -version = "8.1.8" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, -] - [[package]] name = "click" version = "8.2.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", -] dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } wheels = [ @@ -250,16 +221,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2b/fe/4247e732f2234bb5eb9984a0888a70980d681f03cbf433ba7b48f08ca5d5/coverage-7.10.5-cp314-cp314t-win32.whl", hash = "sha256:609b60d123fc2cc63ccee6d17e4676699075db72d14ac3c107cc4976d516f2df", size = 220600, upload-time = "2025-08-23T14:42:22.027Z" }, { url = "https://files.pythonhosted.org/packages/a7/a0/f294cff6d1034b87839987e5b6ac7385bec599c44d08e0857ac7f164ad0c/coverage-7.10.5-cp314-cp314t-win_amd64.whl", hash = "sha256:0666cf3d2c1626b5a3463fd5b05f5e21f99e6aec40a3192eee4d07a15970b07f", size = 221714, upload-time = "2025-08-23T14:42:23.616Z" }, { url = "https://files.pythonhosted.org/packages/23/18/fa1afdc60b5528d17416df440bcbd8fd12da12bfea9da5b6ae0f7a37d0f7/coverage-7.10.5-cp314-cp314t-win_arm64.whl", hash = "sha256:bc85eb2d35e760120540afddd3044a5bf69118a91a296a8b3940dfc4fdcfe1e2", size = 219735, upload-time = "2025-08-23T14:42:25.156Z" }, - { url = "https://files.pythonhosted.org/packages/3b/21/05248e8bc74683488cb7477e6b6b878decadd15af0ec96f56381d3d7ff2d/coverage-7.10.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:62835c1b00c4a4ace24c1a88561a5a59b612fbb83a525d1c70ff5720c97c0610", size = 216763, upload-time = "2025-08-23T14:42:26.75Z" }, - { url = "https://files.pythonhosted.org/packages/a9/7f/161a0ad40cb1c7e19dc1aae106d3430cc88dac3d651796d6cf3f3730c800/coverage-7.10.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5255b3bbcc1d32a4069d6403820ac8e6dbcc1d68cb28a60a1ebf17e47028e898", size = 217154, upload-time = "2025-08-23T14:42:28.238Z" }, - { url = "https://files.pythonhosted.org/packages/de/31/41929ee53af829ea5a88e71d335ea09d0bb587a3da1c5e58e59b48473ed8/coverage-7.10.5-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3876385722e335d6e991c430302c24251ef9c2a9701b2b390f5473199b1b8ebf", size = 243588, upload-time = "2025-08-23T14:42:29.798Z" }, - { url = "https://files.pythonhosted.org/packages/6e/4e/2649344e33eeb3567041e8255a1942173cae81817fe06b60f3fafaafe111/coverage-7.10.5-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8048ce4b149c93447a55d279078c8ae98b08a6951a3c4d2d7e87f4efc7bfe100", size = 245412, upload-time = "2025-08-23T14:42:31.296Z" }, - { url = "https://files.pythonhosted.org/packages/ac/b1/b21e1e69986ad89b051dd42c3ef06d9326e03ac3c0c844fc33385d1d9e35/coverage-7.10.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4028e7558e268dd8bcf4d9484aad393cafa654c24b4885f6f9474bf53183a82a", size = 247182, upload-time = "2025-08-23T14:42:33.155Z" }, - { url = "https://files.pythonhosted.org/packages/4c/b5/80837be411ae092e03fcc2a7877bd9a659c531eff50453e463057a9eee44/coverage-7.10.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:03f47dc870eec0367fcdd603ca6a01517d2504e83dc18dbfafae37faec66129a", size = 245066, upload-time = "2025-08-23T14:42:34.754Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ed/fcb0838ddf149d68d09f89af57397b0dd9d26b100cc729daf1b0caf0b2d3/coverage-7.10.5-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2d488d7d42b6ded7ea0704884f89dcabd2619505457de8fc9a6011c62106f6e5", size = 243138, upload-time = "2025-08-23T14:42:36.311Z" }, - { url = "https://files.pythonhosted.org/packages/75/0f/505c6af24a9ae5d8919d209b9c31b7092815f468fa43bec3b1118232c62a/coverage-7.10.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b3dcf2ead47fa8be14224ee817dfc1df98043af568fe120a22f81c0eb3c34ad2", size = 244095, upload-time = "2025-08-23T14:42:38.227Z" }, - { url = "https://files.pythonhosted.org/packages/e4/7e/c82a8bede46217c1d944bd19b65e7106633b998640f00ab49c5f747a5844/coverage-7.10.5-cp39-cp39-win32.whl", hash = "sha256:02650a11324b80057b8c9c29487020073d5e98a498f1857f37e3f9b6ea1b2426", size = 219289, upload-time = "2025-08-23T14:42:39.827Z" }, - { url = "https://files.pythonhosted.org/packages/9a/ac/46645ef6be543f2e7de08cc2601a0b67e130c816be3b749ab741be689fb9/coverage-7.10.5-cp39-cp39-win_amd64.whl", hash = "sha256:b45264dd450a10f9e03237b41a9a24e85cbb1e278e5a32adb1a303f58f0017f3", size = 220199, upload-time = "2025-08-23T14:42:41.363Z" }, { url = "https://files.pythonhosted.org/packages/08/b6/fff6609354deba9aeec466e4bcaeb9d1ed3e5d60b14b57df2a36fb2273f2/coverage-7.10.5-py3-none-any.whl", hash = "sha256:0be24d35e4db1d23d0db5c0f6a74a962e2ec83c426b5cac09f4234aadef38e4a", size = 208736, upload-time = "2025-08-23T14:42:43.145Z" }, ] @@ -322,7 +283,6 @@ version = "3.1.45" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076, upload-time = "2025-07-24T03:45:54.871Z" } wheels = [ @@ -356,12 +316,12 @@ wheels = [ [[package]] name = "harp-protocol" -version = "0.3.0" +version = "0.4.0" source = { editable = "src/harp-protocol" } [[package]] name = "harp-serial" -version = "0.3.0" +version = "0.4.0" source = { editable = "src/harp-serial" } dependencies = [ { name = "harp-protocol" }, @@ -383,18 +343,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] -[[package]] -name = "importlib-metadata" -version = "8.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, -] - [[package]] name = "iniconfig" version = "2.1.0" @@ -420,9 +368,6 @@ wheels = [ name = "markdown" version = "3.8.2" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, -] sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071, upload-time = "2025-06-19T17:12:44.483Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827, upload-time = "2025-06-19T17:12:42.994Z" }, @@ -484,16 +429,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, - { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344, upload-time = "2024-10-18T15:21:43.721Z" }, - { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389, upload-time = "2024-10-18T15:21:44.666Z" }, - { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607, upload-time = "2024-10-18T15:21:45.452Z" }, - { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728, upload-time = "2024-10-18T15:21:46.295Z" }, - { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826, upload-time = "2024-10-18T15:21:47.134Z" }, - { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843, upload-time = "2024-10-18T15:21:48.334Z" }, - { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219, upload-time = "2024-10-18T15:21:49.587Z" }, - { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946, upload-time = "2024-10-18T15:21:50.441Z" }, - { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063, upload-time = "2024-10-18T15:21:51.385Z" }, - { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506, upload-time = "2024-10-18T15:21:52.974Z" }, ] [[package]] @@ -510,11 +445,9 @@ name = "mkdocs" version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "click" }, { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "ghp-import" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "jinja2" }, { name = "markdown" }, { name = "markupsafe" }, @@ -563,7 +496,6 @@ name = "mkdocs-get-deps" version = "0.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "mergedeep" }, { name = "platformdirs" }, { name = "pyyaml" }, @@ -619,8 +551,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, { name = "backrefs" }, - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "click" }, { name = "colorama" }, { name = "jinja2" }, { name = "markdown" }, @@ -663,7 +594,6 @@ name = "mkdocstrings" version = "0.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "jinja2" }, { name = "markdown" }, { name = "markupsafe" }, @@ -756,6 +686,11 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, +] +docs = [ { name = "griffe-fieldz" }, { name = "mkdocs" }, { name = "mkdocs-codeinclude-plugin" }, @@ -765,9 +700,6 @@ dev = [ { name = "mkdocs-material" }, { name = "mkdocs-monorepo-plugin" }, { name = "mkdocstrings-python" }, - { name = "pytest" }, - { name = "pytest-cov" }, - { name = "ruff" }, ] [package.metadata] @@ -778,6 +710,11 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ + { name = "pytest", specifier = ">=8.3.5" }, + { name = "pytest-cov", specifier = ">=6.1.1" }, + { name = "ruff", specifier = ">=0.11.0" }, +] +docs = [ { name = "griffe-fieldz", specifier = ">=0.2.1" }, { name = "mkdocs", specifier = ">=1.6.1" }, { name = "mkdocs-codeinclude-plugin", specifier = ">=0.2.1" }, @@ -787,9 +724,6 @@ dev = [ { name = "mkdocs-material", specifier = ">=9.6.9" }, { name = "mkdocs-monorepo-plugin", specifier = ">=1.1.2" }, { name = "mkdocstrings-python", specifier = ">=1.16.6" }, - { name = "pytest", specifier = ">=8.3.5" }, - { name = "pytest-cov", specifier = ">=6.1.1" }, - { name = "ruff", specifier = ">=0.11.0" }, ] [[package]] @@ -912,15 +846,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, - { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777, upload-time = "2024-08-06T20:33:25.896Z" }, - { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318, upload-time = "2024-08-06T20:33:27.212Z" }, - { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891, upload-time = "2024-08-06T20:33:28.974Z" }, - { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614, upload-time = "2024-08-06T20:33:34.157Z" }, - { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360, upload-time = "2024-08-06T20:33:35.84Z" }, - { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006, upload-time = "2024-08-06T20:33:37.501Z" }, - { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577, upload-time = "2024-08-06T20:33:39.389Z" }, - { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593, upload-time = "2024-08-06T20:33:46.63Z" }, - { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312, upload-time = "2024-08-06T20:33:49.073Z" }, ] [[package]] @@ -1078,13 +1003,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, - { url = "https://files.pythonhosted.org/packages/05/52/7223011bb760fce8ddc53416beb65b83a3ea6d7d13738dde75eeb2c89679/watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8", size = 96390, upload-time = "2024-11-01T14:06:49.325Z" }, - { url = "https://files.pythonhosted.org/packages/9c/62/d2b21bc4e706d3a9d467561f487c2938cbd881c69f3808c43ac1ec242391/watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a", size = 88386, upload-time = "2024-11-01T14:06:50.536Z" }, - { url = "https://files.pythonhosted.org/packages/ea/22/1c90b20eda9f4132e4603a26296108728a8bfe9584b006bd05dd94548853/watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c", size = 89017, upload-time = "2024-11-01T14:06:51.717Z" }, { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, - { url = "https://files.pythonhosted.org/packages/5b/79/69f2b0e8d3f2afd462029031baafb1b75d11bb62703f0e1022b2e54d49ee/watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa", size = 87903, upload-time = "2024-11-01T14:06:57.052Z" }, - { url = "https://files.pythonhosted.org/packages/e2/2b/dc048dd71c2e5f0f7ebc04dd7912981ec45793a03c0dc462438e0591ba5d/watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e", size = 88381, upload-time = "2024-11-01T14:06:58.193Z" }, { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, @@ -1108,12 +1028,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/79/3e/c0bdc27cf06f4e476 wheels = [ { url = "https://files.pythonhosted.org/packages/eb/d8/0d1d2e9d3fabcf5d6840362adcf05f8cf3cd06a73358140c3a97189238ae/wcmatch-10.1-py3-none-any.whl", hash = "sha256:5848ace7dbb0476e5e55ab63c6bbd529745089343427caa5537f230cc01beb8a", size = 39854, upload-time = "2025-06-22T19:14:00.978Z" }, ] - -[[package]] -name = "zipp" -version = "3.23.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, -] From c61184b2e65b18e8040768d74cfd1b1a589ac164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Fri, 24 Oct 2025 15:10:16 +0100 Subject: [PATCH 179/267] Bump version of packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- src/harp-protocol/pyproject.toml | 2 +- src/harp-serial/pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/harp-protocol/pyproject.toml b/src/harp-protocol/pyproject.toml index c0ce0dd..937e974 100644 --- a/src/harp-protocol/pyproject.toml +++ b/src/harp-protocol/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "harp-protocol" -version = "0.3.0" +version = "0.4.0" description = "Library with the base types for Harp protocol usage." authors = [{ name= "Hardware and Software Platform, Champalimaud Foundation", email="software@research.fchampalimaud.org"}] license = "MIT" diff --git a/src/harp-serial/pyproject.toml b/src/harp-serial/pyproject.toml index 57d7e34..6f311aa 100644 --- a/src/harp-serial/pyproject.toml +++ b/src/harp-serial/pyproject.toml @@ -1,13 +1,13 @@ [project] name = "harp-serial" -version = "0.3.0" +version = "0.4.0" description = "Library for data acquisition and control of devices implementing the Harp protocol." authors = [{ name= "Hardware and Software Platform, Champalimaud Foundation", email="software@research.fchampalimaud.org"}] license = "MIT" keywords = ['python', 'harp'] requires-python = ">=3.10,<4.0" dependencies = [ - "harp-protocol==0.3.0", + "harp-protocol==0.4.0", "pyserial>=3.5", ] From 7523b47b15e87b463229de00e4862c7a4d1d6753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Fri, 24 Oct 2025 15:14:31 +0100 Subject: [PATCH 180/267] Update documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- .github/workflows/build-docs.yml | 11 +- docs/examples/index.md | 2 - .../olfactometer_example.md | 14 --- .../olfactometer_example.py | 116 ------------------ .../wait_for_events/wait_for_events.md | 12 -- .../wait_for_events/wait_for_events.py | 19 --- mkdocs.yml | 18 ++- 7 files changed, 13 insertions(+), 179 deletions(-) delete mode 100644 docs/examples/olfactometer_example/olfactometer_example.md delete mode 100644 docs/examples/olfactometer_example/olfactometer_example.py delete mode 100644 docs/examples/wait_for_events/wait_for_events.md delete mode 100755 docs/examples/wait_for_events/wait_for_events.py diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 72c7f7e..0ad6bea 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -22,7 +22,7 @@ jobs: - name: Install project dependencies run: | - uv sync + uv sync --group docs --group dev - name: Build documentation run: | @@ -34,20 +34,19 @@ jobs: with: path: site - deploy: name: Deploy to Github pages needs: build-docs # Grant GITHUB_TOKEN the permissions required to make a Pages deployment permissions: - pages: write # to deploy to Pages - id-token: write # to verify the deployment originates from an appropriate source + pages: write # to deploy to Pages + id-token: write # to verify the deployment originates from an appropriate source # Deploy to the github-pages environment environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest steps: diff --git a/docs/examples/index.md b/docs/examples/index.md index d045e5d..554e5ba 100644 --- a/docs/examples/index.md +++ b/docs/examples/index.md @@ -5,6 +5,4 @@ This section contains some examples to help you get started with `harp`. Here's the complete list of available examples: - [Getting Device Info](./get_info/get_info.md) - connect to a Harp device and read its information. -- [Wait for Events](./wait_for_events/wait_for_events.md) - connect to a Harp device and wait for events. - [Write and Read from Registers](./read_and_write_from_registers/read_and_write_from_registers.md) - connect to the Harp Behavior, read from a digital input and write to a digital output. -- [Olfactometer Example](./olfactometer_example/olfactometer_example.md) - connect to the Harp Olfactometer, enable flow, open and close the odor valves and monitor the measured flow values. diff --git a/docs/examples/olfactometer_example/olfactometer_example.md b/docs/examples/olfactometer_example/olfactometer_example.md deleted file mode 100644 index 91b1024..0000000 --- a/docs/examples/olfactometer_example/olfactometer_example.md +++ /dev/null @@ -1,14 +0,0 @@ -# Olfactometer Example - -This example shows how to interface with the [Harp Olfactometer](https://github.com/harp-tech/device.olfactometer). - -In this example, the flows for the different channels are enabled to random flow values, then every odor valve is opened, one at a time every 5 seconds, and finally the flow is disabled before closing the connection with the device. During this time, the actual flows in every channel are being printed out in the terminal. - -!!! warning - Don't forget to change the `SERIAL_PORT` to the one that corresponds to your device! The `SERIAL_PORT` must be denoted as `/dev/ttyUSBx` in Linux and `COMx` in Windows, where `x` is the number of the serial port. - - -```python -[](./olfactometer_example.py) -``` - diff --git a/docs/examples/olfactometer_example/olfactometer_example.py b/docs/examples/olfactometer_example/olfactometer_example.py deleted file mode 100644 index 74f089b..0000000 --- a/docs/examples/olfactometer_example/olfactometer_example.py +++ /dev/null @@ -1,116 +0,0 @@ -import random -import time -from threading import Event, Thread - -from serial import SerialException - -from harp.protocol import MessageType, PayloadType -from harp.protocol.messages import HarpMessage -from harp.serial.device import Device, OperationMode - -SERIAL_PORT = ( - "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port) -) - - -def print_events(device, stop_flag): - while not stop_flag.is_set(): - for msg in device.get_events(): - if ( - msg.address == 48 - or msg.address == 49 - or msg.address == 50 - or msg.address == 51 - or msg.address == 52 - ): - print(msg.address - 48) - print(msg.payload[0]) - print() - - -def main(): - # Open connection - device = Device(SERIAL_PORT) - time.sleep(1) - - stop_flag = Event() - - # Check if the device is a Harp Olfactometer - if not device.WHO_AM_I == 1140: - raise SerialException("This is not a Harp Olfactometer.") - - device.set_mode(OperationMode.ACTIVE) - - # Enable flow - device.send(HarpMessage(MessageType.WRITE, PayloadType.U8, 32, 0x01)) - - # Initialize thread for events - events_thread = Thread( - target=print_events, - args=( - device, - stop_flag, - ), - ) - events_thread.start() - - # Set the valves to a random flow - device.send( - HarpMessage( - MessageType.WRITE, PayloadType.FLOAT, 42, int(random.random() * 100) - ) - ) - device.send( - HarpMessage( - MessageType.WRITE, PayloadType.FLOAT, 43, int(random.random() * 100) - ) - ) - device.send( - HarpMessage( - MessageType.WRITE, PayloadType.FLOAT, 44, int(random.random() * 100) - ) - ) - device.send( - HarpMessage( - MessageType.WRITE, PayloadType.FLOAT, 45, int(random.random() * 100) - ) - ) - - # Open every odor valve, one at a time every 5 seconds - device.send(HarpMessage(MessageType.WRITE, PayloadType.FLOAT, 68, 0x01)) - - time.sleep(5) - - device.send(HarpMessage(MessageType.WRITE, PayloadType.FLOAT, 69, 0x01)) - device.send(HarpMessage(MessageType.WRITE, PayloadType.FLOAT, 68, 0x02)) - - time.sleep(5) - - device.send(HarpMessage(MessageType.WRITE, PayloadType.FLOAT, 69, 0x02)) - device.send(HarpMessage(MessageType.WRITE, PayloadType.FLOAT, 68, 0x04)) - - time.sleep(5) - - device.send(HarpMessage(MessageType.WRITE, PayloadType.FLOAT, 69, 0x04)) - device.send(HarpMessage(MessageType.WRITE, PayloadType.FLOAT, 68, 0x08)) - - time.sleep(5) - - device.send(HarpMessage(MessageType.WRITE, PayloadType.FLOAT, 69, 0x08)) - - time.sleep(5) - - # Disable flow - device.send(HarpMessage(MessageType.WRITE, PayloadType.FLOAT, 32, 0x00)) - - time.sleep(1) - - stop_flag.set() - events_thread.join() - - # Close connection - device.disconnect() - - -if __name__ == "__main__": - main() diff --git a/docs/examples/wait_for_events/wait_for_events.md b/docs/examples/wait_for_events/wait_for_events.md deleted file mode 100644 index 8db54fa..0000000 --- a/docs/examples/wait_for_events/wait_for_events.md +++ /dev/null @@ -1,12 +0,0 @@ -# Wait for Events - -This example demonstrates how to read the events sent by the Harp device. - -!!! warning - Don't forget to change the `SERIAL_PORT` to the one that corresponds to your device! The `SERIAL_PORT` must be denoted as `/dev/ttyUSBx` in Linux and `COMx` in Windows, where `x` is the number of the serial port. - - -```python -[](./wait_for_events.py) -``` - diff --git a/docs/examples/wait_for_events/wait_for_events.py b/docs/examples/wait_for_events/wait_for_events.py deleted file mode 100755 index e830e3d..0000000 --- a/docs/examples/wait_for_events/wait_for_events.py +++ /dev/null @@ -1,19 +0,0 @@ -from harp.protocol import OperationMode -from harp.serial.device import Device - -SERIAL_PORT = ( - "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port) -) - -# Open serial connection and save communication to a file -device = Device(SERIAL_PORT, "dump.bin") - -# Set device to Active Mode -device.set_mode(OperationMode.ACTIVE) -print("Setting mode to active.") - -# Read device's events -while True: - for msg in device.get_events(): - print(msg) - print() diff --git a/mkdocs.yml b/mkdocs.yml index 32e9df3..8b34537 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,6 +1,6 @@ site_name: pyharp -repo_url: "https://github.com/fchampalimaud/pyharp" -# repo_name: "pyharp" +repo_url: https://github.com/fchampalimaud/pyharp +repo_name: "fchampalimaud/pyharp" copyright: Copyright © 2025, Hardware and Software Platform, Champalimaud Foundation plugins: @@ -50,9 +50,8 @@ theme: - content.tooltips - toc.follow - content.code.copy - # - navigation.sections + - navigation.sections - navigation.indexes - - navigation.expand - navigation.footer palette: - media: "(prefers-color-scheme)" @@ -76,16 +75,15 @@ theme: nav: - Home: index.md - - API: - - Protocol: api/protocol.md - - Serial: api/serial.md - Examples: - examples/index.md - Getting Device Info: examples/get_info/get_info.md - - Wait for Events: examples/wait_for_events/wait_for_events.md - Read and Write from Registers: examples/read_and_write_from_registers/read_and_write_from_registers.md - - Olfactometer Example: examples/olfactometer_example/olfactometer_example.md - - Devices: '*include ./harp.devices/*/mkdocs.yml' + - Devices: '*include ./harp.devices/*/docs/examples.mkdocs.yml' + - API: + - Devices: '*include ./harp.devices/*/mkdocs.yml' + - Protocol: api/protocol.md + - Serial: api/serial.md extra: social: From 6f5cde3dff4acd77eadb953315b54fe01c571a75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Teixeira?= Date: Wed, 5 Nov 2025 10:57:08 +0000 Subject: [PATCH 181/267] Update docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Grilo --- src/harp-protocol/harp/protocol/base.py | 31 +++++++++++++++++-- src/harp-protocol/harp/protocol/exceptions.py | 30 ++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/harp-protocol/harp/protocol/base.py b/src/harp-protocol/harp/protocol/base.py index 88b5b88..7419d3c 100644 --- a/src/harp-protocol/harp/protocol/base.py +++ b/src/harp-protocol/harp/protocol/base.py @@ -30,7 +30,14 @@ class MessageType(IntEnum): READ_ERROR = READ | ERROR WRITE_ERROR = WRITE | ERROR - def is_error(self): + def is_error(self) -> bool: + """ + Check if the message type is an error message. + Returns + ------- + bool + Returns True if the message type is an error message, False otherwise. + """ return bool(self & MessageType.ERROR) @@ -123,6 +130,10 @@ class PayloadType(IntEnum): def has_timestamp(self): """ + Check if this PayloadType has a timestamp. + + Returns + ------- bool Returns True if this PayloadType has a timestamp, False otherwise. """ @@ -130,19 +141,35 @@ def has_timestamp(self): def is_float(self): """ + Check if this `PayloadType` is a float. + + Returns + ------- bool - Returns True if this PayloadType is a float, False otherwise. + Returns True if this `PayloadType` is a float, False otherwise. """ return bool(self & _PayloadTypeFlags.IS_FLOAT) def is_signed(self): """ + Check if this PayloadType is signed. + + Returns + ------- bool Returns True if this PayloadType is signed, False otherwise. """ return bool(self & _PayloadTypeFlags.IS_SIGNED) def type_size(self): + """ + Get the size of this PayloadType in bytes. + + Returns + ------- + int + The size of this PayloadType in bytes. + """ return self & _PayloadTypeFlags.TYPE_SIZE diff --git a/src/harp-protocol/harp/protocol/exceptions.py b/src/harp-protocol/harp/protocol/exceptions.py index dc095a0..0e9eb90 100644 --- a/src/harp-protocol/harp/protocol/exceptions.py +++ b/src/harp-protocol/harp/protocol/exceptions.py @@ -7,6 +7,16 @@ class HarpException(Exception): """Base class for all exceptions raised related with Harp.""" def __init__(self, error_msg: str, message: Optional[HarpMessage] = None): + """ + Creates a new HarpException with the given error message. + + Parameters + ---------- + error_msg: str + The error message describing the exception. + message: Optional[HarpMessage] + The Harp message that caused the exception, if applicable. + """ super().__init__(error_msg) self.message = message @@ -17,6 +27,16 @@ class HarpWriteException(HarpException): """ def __init__(self, register_str: str, message: HarpMessage): + """ + Creates a new HarpWriteException for the given register. + + Parameters + ---------- + register_str: str + The register string where the write error occurred. + message: HarpMessage + The Harp message that caused the write error. + """ super().__init__(f"Error writing to device on address {register_str}.", message) @@ -26,6 +46,16 @@ class HarpReadException(HarpException): """ def __init__(self, register_str: str, message: HarpMessage): + """ + Creates a new HarpReadException for the given register. + + Parameters + ---------- + register_str: str + The register string where the read error occurred. + message: HarpMessage + The Harp message that caused the read error. + """ super().__init__(f'Error reading from register "{register_str}".', message) From bbe437dea0230f196bf38476e0a15b0ada205a02 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sun, 26 Apr 2026 15:50:10 -0700 Subject: [PATCH 182/267] Linting --- docs/examples/get_info/get_info.py | 4 +--- .../read_and_write_from_registers.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/examples/get_info/get_info.py b/docs/examples/get_info/get_info.py index ea88143..f1d4bd9 100755 --- a/docs/examples/get_info/get_info.py +++ b/docs/examples/get_info/get_info.py @@ -1,8 +1,6 @@ from pyharp.protocol.device import Device -SERIAL_PORT = ( - "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port) -) +SERIAL_PORT = "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port) # Open serial connection and save communication to a file device = Device(SERIAL_PORT, "dump.bin") diff --git a/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py b/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py index efa4664..d38e86c 100755 --- a/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py +++ b/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py @@ -4,9 +4,7 @@ from harp.protocol.messages import HarpMessage from harp.serial.device import Device -SERIAL_PORT = ( - "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port) -) +SERIAL_PORT = "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port) # Open serial connection and save communication to a file device = Device(SERIAL_PORT, "dump.bin") From 1e7986e0fc99aa7b73cc582fe1f6912055cccd3d Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sun, 26 Apr 2026 15:51:14 -0700 Subject: [PATCH 183/267] Replace serial by device package --- src/harp-device/pyproject.toml | 17 + src/harp-device/src/harp/device/__init__.py | 45 + src/harp-device/src/harp/device/_device.py | 34 + src/harp-device/src/harp/device/_registers.py | 144 ++ src/harp-device/src/harp/device/_serial.py | 105 ++ src/harp-device/src/harp/device/py.typed | 0 src/harp-serial/README.md | 75 - src/harp-serial/harp/serial/__init__.py | 3 - src/harp-serial/harp/serial/device.py | 1347 ----------------- src/harp-serial/harp/serial/harp_serial.py | 160 -- src/harp-serial/pyproject.toml | 23 - 11 files changed, 345 insertions(+), 1608 deletions(-) create mode 100644 src/harp-device/pyproject.toml create mode 100644 src/harp-device/src/harp/device/__init__.py create mode 100644 src/harp-device/src/harp/device/_device.py create mode 100644 src/harp-device/src/harp/device/_registers.py create mode 100644 src/harp-device/src/harp/device/_serial.py create mode 100644 src/harp-device/src/harp/device/py.typed delete mode 100644 src/harp-serial/README.md delete mode 100644 src/harp-serial/harp/serial/__init__.py delete mode 100644 src/harp-serial/harp/serial/device.py delete mode 100644 src/harp-serial/harp/serial/harp_serial.py delete mode 100644 src/harp-serial/pyproject.toml diff --git a/src/harp-device/pyproject.toml b/src/harp-device/pyproject.toml new file mode 100644 index 0000000..58d5a62 --- /dev/null +++ b/src/harp-device/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "harp-device" +version = "0.1.0" +description = "Harp device interfaces — serial and other transports" +requires-python = ">=3.11" +dependencies = [ + "harp-protocol", + "pyserial>=3.5", +] + +[build-system] +requires = ["uv_build>=0.9.5,<0.10.0"] +build-backend = "uv_build" + +[tool.uv.build-backend] +module-name = "harp.device" +module-root = "src" \ No newline at end of file diff --git a/src/harp-device/src/harp/device/__init__.py b/src/harp-device/src/harp/device/__init__.py new file mode 100644 index 0000000..f6fc2fd --- /dev/null +++ b/src/harp-device/src/harp/device/__init__.py @@ -0,0 +1,45 @@ +from ._device import Device +from ._registers import ( + AssemblyVersion, + ClockConfig, + CoreVersionH, + CoreVersionL, + FirmwareVersionH, + FirmwareVersionL, + Heartbeat, + HwVersionH, + HwVersionL, + OperationControl, + OperationControlPayload, + OperationMode, + ResetDevice, + SerialNumber, + TimestampMicro, + TimestampOffset, + TimestampSecond, + WhoAmI, +) +from ._serial import SerialDevice + +__all__ = [ + "Device", + "SerialDevice", + "WhoAmI", + "HwVersionH", + "HwVersionL", + "AssemblyVersion", + "CoreVersionH", + "CoreVersionL", + "FirmwareVersionH", + "FirmwareVersionL", + "TimestampSecond", + "TimestampMicro", + "OperationControl", + "OperationControlPayload", + "OperationMode", + "ResetDevice", + "SerialNumber", + "ClockConfig", + "TimestampOffset", + "Heartbeat", +] diff --git a/src/harp-device/src/harp/device/_device.py b/src/harp-device/src/harp/device/_device.py new file mode 100644 index 0000000..2548e18 --- /dev/null +++ b/src/harp-device/src/harp/device/_device.py @@ -0,0 +1,34 @@ +"""High-level Harp device API.""" + +from typing import Any, TypeVar + +from harp.protocol._message import ParsedHarpMessage +from harp.protocol._payload import PayloadBase +from harp.protocol._register import RegisterBase + +from ._serial import SerialDevice + +P = TypeVar("P", bound=PayloadBase[Any]) + + +class Device: + def __init__(self, port: str, baudrate: int = SerialDevice.DEFAULT_BAUDRATE) -> None: + self._dev = SerialDevice(port, baudrate) + + def read(self, register: type[RegisterBase[P]]) -> ParsedHarpMessage[P]: + # Note: ty can't correctly infer the return type, and this is a known issue: + # https://github.com/astral-sh/ty/issues/623 + return self._dev.read_register(register) + + def write(self, register: type[RegisterBase[P]], value: Any) -> ParsedHarpMessage[P]: + return self._dev.write_register(register, value) + + def close(self) -> None: + self._dev.close() + + def __enter__(self) -> "Device": + return self + + def __exit__(self, *args: object) -> None: + self.close() + self.close() diff --git a/src/harp-device/src/harp/device/_registers.py b/src/harp-device/src/harp/device/_registers.py new file mode 100644 index 0000000..df179d1 --- /dev/null +++ b/src/harp-device/src/harp/device/_registers.py @@ -0,0 +1,144 @@ +import enum +from typing import ClassVar + +import numpy as np +import pandas as pd +from harp.protocol._payload import PayloadBase +from harp.protocol._payload_type import PayloadType +from harp.protocol._register import RegisterBase, RegisterU8, RegisterU16, RegisterU32 +from numpy.typing import NDArray + + +class OperationMode(enum.IntEnum): + Standby = 0 + Active = 1 + Speed = 2 + Reserved = 3 + + +class OperationControlPayload(PayloadBase[np.void]): + _dtype: ClassVar = np.dtype([("operation_control", "u1")]) + + def __init__( + self, + *, + operation_mode: int | np.uint8 = 0, + dump_registers: bool = False, + mute_replies: bool = False, + visual_indicators: int | np.uint8 = 0, + operation_led: int | np.uint8 = 0, + heartbeat: int | np.uint8 = 0, + ) -> None: + arr = np.zeros(1, dtype=self._dtype) + arr["operation_control"] |= np.uint8(operation_mode) & np.uint8(0x03) + arr["operation_control"] |= np.uint8(dump_registers) << np.uint8(3) + arr["operation_control"] |= np.uint8(mute_replies) << np.uint8(4) + arr["operation_control"] |= (np.uint8(visual_indicators) & np.uint8(0x01)) << np.uint8(5) + arr["operation_control"] |= (np.uint8(operation_led) & np.uint8(0x01)) << np.uint8(6) + arr["operation_control"] |= (np.uint8(heartbeat) & np.uint8(0x01)) << np.uint8(7) + self._arr = arr + + @property + def operation_mode(self) -> NDArray[np.uint8]: + return self._arr["operation_control"] & np.uint8(0x03) + + @property + def dump_registers(self) -> NDArray[np.bool_]: + return (self._arr["operation_control"] & np.uint8(0x08)) != 0 + + @property + def mute_replies(self) -> NDArray[np.bool_]: + return (self._arr["operation_control"] & np.uint8(0x10)) != 0 + + @property + def visual_indicators(self) -> NDArray[np.bool_]: + return ((self._arr["operation_control"] >> np.uint8(5)) & np.uint8(0x01)) != 0 + + @property + def operation_led(self) -> NDArray[np.bool_]: + return ((self._arr["operation_control"] >> np.uint8(6)) & np.uint8(0x01)) != 0 + + @property + def heartbeat(self) -> NDArray[np.bool_]: + return ((self._arr["operation_control"] >> np.uint8(7)) & np.uint8(0x01)) != 0 + + def to_dataframe(self) -> pd.DataFrame: + return pd.DataFrame( + { + "operation_mode": self.operation_mode, + "dump_registers": self.dump_registers, + "mute_replies": self.mute_replies, + "visual_indicators": self.visual_indicators, + "operation_led": self.operation_led, + "heartbeat": self.heartbeat, + } + ) + + +class WhoAmI(RegisterU16): + address: ClassVar[int] = 0 + + +class TimestampSecond(RegisterU32): + address: ClassVar[int] = 8 + + +class TimestampMicro(RegisterU16): + address: ClassVar[int] = 9 + + +class OperationControl(RegisterBase[OperationControlPayload]): + address: ClassVar[int] = 10 + payload_type: ClassVar[PayloadType] = PayloadType.U8 + payload_class: ClassVar[type[PayloadBase]] = OperationControlPayload + + +class ResetDevice(RegisterU8): + address: ClassVar[int] = 11 + + +class ClockConfig(RegisterU8): + address: ClassVar[int] = 14 + + +class Heartbeat(RegisterU16): + address: ClassVar[int] = 18 + + +# ── Deprecated ─────────────────────── + + +class HwVersionH(RegisterU8): + address: ClassVar[int] = 1 + + +class HwVersionL(RegisterU8): + address: ClassVar[int] = 2 + + +class AssemblyVersion(RegisterU8): + address: ClassVar[int] = 3 + + +class CoreVersionH(RegisterU8): + address: ClassVar[int] = 4 + + +class CoreVersionL(RegisterU8): + address: ClassVar[int] = 5 + + +class FirmwareVersionH(RegisterU8): + address: ClassVar[int] = 6 + + +class FirmwareVersionL(RegisterU8): + address: ClassVar[int] = 7 + + +class SerialNumber(RegisterU16): + address: ClassVar[int] = 13 + + +class TimestampOffset(RegisterU8): + address: ClassVar[int] = 15 diff --git a/src/harp-device/src/harp/device/_serial.py b/src/harp-device/src/harp/device/_serial.py new file mode 100644 index 0000000..b81201f --- /dev/null +++ b/src/harp-device/src/harp/device/_serial.py @@ -0,0 +1,105 @@ +import queue +import threading +from typing import Any, ClassVar, Self, TypeVar + +import serial +from harp.protocol import HarpFramer, HarpMessage, MessageType +from harp.protocol._message import ParsedHarpMessage +from harp.protocol._payload import PayloadBase +from harp.protocol._register import RegisterBase + +P = TypeVar("P", bound=PayloadBase[Any]) + + +class SerialDevice: + DEFAULT_BAUDRATE: ClassVar[int] = 1_000_000 + REPLY_TIMEOUT: float = 5.0 # seconds + + def __init__(self, port: str, baudrate: int = DEFAULT_BAUDRATE) -> None: + self._serial = serial.Serial(port, baudrate, timeout=0.1) + self._serial.dtr = True + + self._framer = HarpFramer() + self._pending: dict[int, queue.SimpleQueue] = {} + self._pending_lock = threading.Lock() + self._running = True + + self._thread = threading.Thread( + target=self._read_loop, daemon=True, name="harp-serial-device" + ) + self._thread.start() + + def read_register(self, register: type[RegisterBase[P]]) -> ParsedHarpMessage[P]: + frame = register.format() + msg = self._request(register.address, frame) + return ParsedHarpMessage.from_message(msg, register.parse(msg)) + + def write_register(self, register: type[RegisterBase[P]], value: Any) -> ParsedHarpMessage[P]: + frame = register.format(value) + msg = self._request(register.address, frame) + return ParsedHarpMessage.from_message(msg, register.parse(msg)) + + def close(self) -> None: + self._running = False + self._thread.join(timeout=2.0) + try: + self._serial.dtr = False + except Exception: + pass + self._serial.close() + + def __enter__(self) -> Self: + return self + + def __exit__(self, *args: object) -> None: + self.close() + + def _on_event(self, msg: HarpMessage) -> None: + """Called from the reader thread when an Event message arrives. + + Override in subclasses to handle device-specific events. + The default implementation discards the message. + """ + + def _read_loop(self) -> None: + while self._running: + try: + waiting = self._serial.in_waiting + chunk = self._serial.read(waiting if waiting > 0 else 1) + except serial.SerialException: + if self._running: + raise + break + + if not chunk: + continue + + self._framer.feed(chunk) + for msg in self._framer.frames(): + self._dispatch(msg) + + def _dispatch(self, msg: HarpMessage) -> None: + if msg.message_type in (MessageType.Read, MessageType.Write): + with self._pending_lock: + q = self._pending.get(msg.address) + if q is not None: + q.put(msg) + elif msg.message_type == MessageType.Event: + self._on_event(msg) + + def _request(self, address: int, frame: bytes) -> HarpMessage: + q: queue.SimpleQueue = queue.SimpleQueue() + with self._pending_lock: + self._pending[address] = q + try: + self._serial.write(frame) + try: + return q.get(timeout=self.REPLY_TIMEOUT) + except queue.Empty as exc: + raise TimeoutError( + f"No reply from device for register address {address} " + f"within {self.REPLY_TIMEOUT}s" + ) from exc + finally: + with self._pending_lock: + self._pending.pop(address, None) diff --git a/src/harp-device/src/harp/device/py.typed b/src/harp-device/src/harp/device/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/harp-serial/README.md b/src/harp-serial/README.md deleted file mode 100644 index e9608bb..0000000 --- a/src/harp-serial/README.md +++ /dev/null @@ -1,75 +0,0 @@ -# harp-serial - -[![PyPI version](https://badge.fury.io/py/harp-serial.svg)](https://badge.fury.io/py/harp-serial) - -A Python library for communicating with Harp devices over serial connections. - -## Installation - -```bash -uv add harp-serial -# or -pip install harp-serial -``` - -## Quick Start - -```python -from harp.protocol import MessageType, PayloadType -from harp.protocol.messages import HarpMessage -from harp.serial.device import Device - -# Connect to a device -device = Device("/dev/ttyUSB0") -#device = Device("COM3") # for Windows - -# Get device information -device.info() - -# define register_address -register_address = 32 - -# Read from register -reply = device.send(HarpMessage(MessageType.READ, PayloadType.U8, register_address)) - -# Write to register -device.send(HarpMessage(MessageType.WRITE, PayloadType.U8, register_address, reply.payload)) - -# Disconnect when done -device.disconnect() -``` - -or using the `with` statement: - -```python -from harp.protocol import MessageType, PayloadType -from harp.protocol.messages import HarpMessage -from harp.serial.device import Device - -with Device("/dev/ttyUSB0") as device: - # Get device information - device.info() - - # define register_address - register_address = 32 - - # Read from register - reply = device.send(HarpMessage(MessageType.READ, PayloadType.U8, register_address)) - - # Write to register - device.send(HarpMessage(MessageType.WRITE, PayloadType.U8, register_address, reply.payload)) -``` - -## for Linux - -### Install UDEV Rules - -Install by either copying `10-harp.rules` over to your `/etc/udev/rules.d` folder or by symlinking it with: -```` -sudo ln -s /absolute/path/to/10-harp.rules /etc/udev/rules.d/10-harp.rules -```` - -Then reload udev rules with -```` -sudo udevadm control --reload-rules -```` diff --git a/src/harp-serial/harp/serial/__init__.py b/src/harp-serial/harp/serial/__init__.py deleted file mode 100644 index 811fc78..0000000 --- a/src/harp-serial/harp/serial/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .device import Device as Device -from .device import TimeoutStrategy as TimeoutStrategy -from .harp_serial import HarpSerial as HarpSerial diff --git a/src/harp-serial/harp/serial/device.py b/src/harp-serial/harp/serial/device.py deleted file mode 100644 index 3167c36..0000000 --- a/src/harp-serial/harp/serial/device.py +++ /dev/null @@ -1,1347 +0,0 @@ -from __future__ import annotations # for type hints (PEP 563) - -import logging -import queue -from enum import Enum -from io import BufferedWriter -from pathlib import Path -from typing import Optional - -import serial -from harp.protocol import ( - ClockConfig, - CommonRegisters, - MessageType, - OperationCtrl, - OperationMode, - PayloadType, - ResetMode, -) -from harp.protocol.exceptions import HarpException, HarpTimeoutException -from harp.protocol.messages import HarpMessage -from harp.serial.harp_serial import HarpSerial - - -class TimeoutStrategy(Enum): - """ - Strategy to handle timeouts when waiting for a reply from the device. - - Attributes - ---------- - RAISE : str - Raise HarpTimeoutException - RETURN_NONE : str - Return None - LOG_AND_RAISE : str - Log the timeout and raise HarpTimeoutException - LOG_AND_NONE : str - Log the timeout and return None - """ - - RAISE = "raise" # Raise HarpTimeoutException - RETURN_NONE = "return_none" # Return None - LOG_AND_RAISE = "log_and_raise" - LOG_AND_NONE = "log_and_none" - - -class Device: - """ - The `Device` class provides the interface for interacting with Harp devices. This implementation of the Harp device was based on the official documentation available on the [harp-tech website](https://harp-tech.org/protocol/Device.html). - - Attributes - ---------- - WHO_AM_I : int - The device ID number. A list of devices can be found [here](https://github.com/harp-tech/protocol/blob/main/whoami.md) - HW_VERSION_H : int - The major hardware version - HW_VERSION_L : int - The minor hardware version - ASSEMBLY_VERSION : int - The version of the assembled components - HARP_VERSION_H : int - The major Harp core version - HARP_VERSION_L : int - The minor Harp core version - FIRMWARE_VERSION_H : int - The major firmware version - FIRMWARE_VERSION_L : int - The minor firmware version - DEVICE_NAME : str - The device name stored in the Harp device - SERIAL_NUMBER : int, optional - The serial number of the device - """ - - WHO_AM_I: int - HW_VERSION_H: int - HW_VERSION_L: int - ASSEMBLY_VERSION: int - CORE_VERSION_H: int - CORE_VERSION_L: int - FIRMWARE_VERSION_H: int - FIRMWARE_VERSION_L: int - DEVICE_NAME: str - SERIAL_NUMBER: int - CLOCK_CONFIG: int - TIMESTAMP_OFFSET: int - - _ser: HarpSerial - _dump_file_path: Optional[Path] - _dump_file: Optional[BufferedWriter] = None - _timeout: float - - def __init__( - self, - serial_port: str, - dump_file_path: Optional[str] = None, - timeout: float = 1, - timeout_strategy: TimeoutStrategy = TimeoutStrategy.RAISE, - ): - """ - Parameters - ---------- - serial_port : str - The serial port used to establish the connection with the Harp device. It must be denoted as `/dev/ttyUSBx` in Linux and `COMx` in Windows, where `x` is the number of the serial port - dump_file_path: str, optional - The binary file to which all Harp messages will be written - timeout: float, optional - The timeout in seconds when waiting for a reply from the device - timeout_strategy: TimeoutStrategy, optional - The strategy to handle timeouts when waiting for a reply from the device - """ - self.log = logging.getLogger(f"{__name__}.{self.__class__.__name__}") - self._serial_port = serial_port - self._dump_file_path = None - if dump_file_path is not None: - self._dump_file_path = Path() / dump_file_path - self._timeout = timeout - self._timeout_strategy = timeout_strategy - - # Connect to the Harp device and load the data stored in the device's common registers - self.connect() - self.load() - - def load(self) -> None: - """ - Loads the data stored in the device's common registers. - """ - self.WHO_AM_I = self._read_who_am_i() - self.HW_VERSION_H = self._read_hw_version_h() - self.HW_VERSION_L = self._read_hw_version_l() - self.ASSEMBLY_VERSION = self._read_assembly_version() - self.CORE_VERSION_H = self._read_core_version_h() - self.CORE_VERSION_L = self._read_core_version_l() - self.FIRMWARE_VERSION_H = self._read_fw_version_h() - self.FIRMWARE_VERSION_L = self._read_fw_version_l() - self.DEVICE_NAME = self._read_device_name() - self.SERIAL_NUMBER = self._read_serial_number() - self.CLOCK_CONFIG = self._read_clock_config() - self.TIMESTAMP_OFFSET = self._read_timestamp_offset() - - def info(self) -> None: - """ - Prints the device information. - """ - print("Device info:") - print(f"* Who am I: ({self.WHO_AM_I})") - print(f"* HW version: {self.HW_VERSION_H}.{self.HW_VERSION_L}") - print(f"* Assembly version: {self.ASSEMBLY_VERSION}") - print(f"* HARP version: {self.CORE_VERSION_H}.{self.CORE_VERSION_L}") - print( - f"* Firmware version: {self.FIRMWARE_VERSION_H}.{self.FIRMWARE_VERSION_L}" - ) - print(f"* Device name: {self.DEVICE_NAME}") - print(f"* Serial number: {self.SERIAL_NUMBER}") - print(f"* Mode: {self._read_device_mode().name}") - - def connect(self) -> None: - """ - Connects to the Harp device. - """ - self._ser = HarpSerial( - self._serial_port, - baudrate=1000000, - timeout=self._timeout, - parity=serial.PARITY_NONE, - stopbits=1, - bytesize=8, - rtscts=True, - ) - - # open file if it is defined - if self._dump_file_path is not None: - self._dump_file = open(self._dump_file_path, "ab") - - def disconnect(self) -> None: - """ - Disconnects from the Harp device. - """ - # close file if it exists - if self._dump_file: - self._dump_file.close() - self._dump_file = None - - self._ser.close() - - def _read_device_mode(self) -> OperationMode: - """ - Reads the current operation mode of the Harp device. - - Returns - ------- - DeviceMode - The current device mode - """ - address = CommonRegisters.OPERATION_CTRL - reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) - return OperationMode(reply.payload & OperationCtrl.OP_MODE) - - def dump_registers(self) -> list: - """ - Asserts the DUMP bit to dump the values of all core and app registers - as Harp Read Reply Messages. More information on the DUMP bit can be found [here](https://harp-tech.org/protocol/Device.html#r_operation_ctrl-u16--operation-mode-configuration). - - Returns - ------- - list - The list containing the reply Harp messages for all the device's registers - """ - address = CommonRegisters.OPERATION_CTRL - reg_value = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) - - if reg_value is None: - return [] - - reg_value = reg_value.payload - - # Assert DUMP bit - reg_value |= OperationCtrl.DUMP - self.send(HarpMessage(MessageType.WRITE, PayloadType.U8, address, reg_value)) - - # Receive the contents of all registers as Harp Read Reply Messages - replies = [] - while True: - msg = self._read() - if msg is None: - break - else: - replies.append(msg) - self._dump_reply(msg.frame) - return replies - - def read_operation_ctrl(self): - """ - Reads the OPERATION_CTRL register of the device. - - Returns - ------- - HarpMessage - The reply to the Harp message - """ - address = CommonRegisters.OPERATION_CTRL - reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) - - # create dict with complete byte and then decode each bit according to the OperationCtrl entries - if reply is not None: - reg_value = reply.payload - result = { - "REG_VALUE": reply.payload, - "OP_MODE": OperationMode(reg_value & OperationCtrl.OP_MODE), - "DUMP": bool(reg_value & OperationCtrl.DUMP), - "MUTE_RPL": bool(reg_value & OperationCtrl.MUTE_RPL), - "VISUALEN": bool(reg_value & OperationCtrl.VISUALEN), - "OPLEDEN": bool(reg_value & OperationCtrl.OPLEDEN), - "ALIVE_EN": bool(reg_value & OperationCtrl.ALIVE_EN), - } - return result - - def write_operation_ctrl( - self, - mode: Optional[OperationMode] = None, - mute_rpl: Optional[bool] = None, - visual_en: Optional[bool] = None, - op_led_en: Optional[bool] = None, - alive_en: Optional[bool] = None, - ) -> HarpMessage | None: - """ - Writes the OPERATION_CTRL register of the device. - - Parameters - ---------- - mode : OperationMode, optional - The new operation mode value - mute_rpl : bool, optional - If True, the Replies to all the Commands are muted - visual_en : bool, optional - If True, enables the status led - op_led_en : bool, optional - If True, enables the operation LED - alive_en : bool, optional - If True, enables the ALIVE_EN bit - Returns - ------- - HarpMessage - The reply to the Harp message - """ - address = CommonRegisters.OPERATION_CTRL - - # Read register first - reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) - - if reply is None: - return reply - - reg_value = reply.payload - - if mode is not None: - # Clear old operation mode - reg_value &= ~OperationCtrl.OP_MODE - # Set new operation mode - reg_value |= mode - - if mute_rpl is not None: - if mute_rpl: - reg_value |= OperationCtrl.MUTE_RPL - else: - reg_value &= ~OperationCtrl.MUTE_RPL - - if visual_en is not None: - if visual_en: - reg_value |= OperationCtrl.VISUALEN - else: - reg_value &= ~OperationCtrl.VISUALEN - - if op_led_en is not None: - if op_led_en: - reg_value |= OperationCtrl.OPLEDEN - else: - reg_value &= ~OperationCtrl.OPLEDEN - - if alive_en is not None: - if alive_en: - reg_value |= OperationCtrl.ALIVE_EN - else: - reg_value &= ~OperationCtrl.ALIVE_EN - - reply = self.send( - HarpMessage(MessageType.WRITE, PayloadType.U8, address, reg_value) - ) - - return reply - - def set_mode(self, mode: OperationMode) -> HarpMessage | None: - """ - Sets the operation mode of the device. - - Parameters - ---------- - mode : DeviceMode - The new device mode value - - Returns - ------- - HarpMessage - The reply to the Harp message - """ - address = CommonRegisters.OPERATION_CTRL - - # Read register first - reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) - - if reply is None: - return reply - - reg_value = reply.payload - - # Clear old operation mode - reg_value &= ~OperationCtrl.OP_MODE - - # Set new operation mode - reg_value |= mode - reply = self.send( - HarpMessage(MessageType.WRITE, PayloadType.U8, address, reg_value) - ) - - return reply - - def alive_en(self, enable: bool) -> bool: - """ - Sets the ALIVE_EN bit of the device. - - Parameters - ---------- - enable : bool - If True, enables the ALIVE_EN bit. If False, disables it - - Returns - ------- - bool - True if the operation was successful, False otherwise - """ - address = CommonRegisters.OPERATION_CTRL - - # Read register first - reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) - - if reply is None: - return False - - reg_value = reply.payload - - if enable: - reg_value |= OperationCtrl.ALIVE_EN - else: - reg_value &= ~OperationCtrl.ALIVE_EN - - reply = self.send( - HarpMessage(MessageType.WRITE, PayloadType.U8, address, reg_value) - ) - - if reply is None: - return False - - return reply is not None - - def op_led_en(self, enable: bool) -> bool: - """ - Sets the operation LED of the device. - - Parameters - ---------- - enable : bool - If True, enables the operation LED. If False, disables it - - Returns - ------- - bool - True if the operation was successful, False otherwise - """ - address = CommonRegisters.OPERATION_CTRL - - # Read register first - reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) - - if reply is None: - return False - - reg_value = reply.payload - - if enable: - reg_value |= OperationCtrl.OPLEDEN - else: - reg_value &= ~OperationCtrl.OPLEDEN - - reply = self.send( - HarpMessage(MessageType.WRITE, PayloadType.U8, address, reg_value) - ) - - return reply is not None - - def visual_en(self, enable: bool) -> bool: - """ - Sets the status led of the device. - - Parameters - ---------- - enable : bool - If True, enables the status led. If False, disables it - - Returns - ------- - bool - True if the operation was successful, False otherwise - """ - address = CommonRegisters.OPERATION_CTRL - - # Read register first - reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) - - if reply is None: - return False - - reg_value = reply.payload - - if enable: - reg_value |= OperationCtrl.VISUALEN - else: - reg_value &= ~OperationCtrl.VISUALEN - - reply = self.send( - HarpMessage(MessageType.WRITE, PayloadType.U8, address, reg_value) - ) - - return reply is not None - - def mute_reply(self, enable: bool) -> bool: - """ - Sets the MUTE_REPLY bit of the device. - - Parameters - ---------- - enable : bool - If True, the Replies to all the Commands are muted. If False, un-mutes them - - Returns - ------- - bool - True if the operation was successful, False otherwise - """ - address = CommonRegisters.OPERATION_CTRL - - # Read register first - reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) - - if reply is None: - return False - - reg_value = reply.payload - - if enable: - reg_value |= OperationCtrl.MUTE_RPL - else: - reg_value &= ~OperationCtrl.MUTE_RPL - - reply = self.send( - HarpMessage(MessageType.WRITE, PayloadType.U8, address, reg_value) - ) - - return reply is not None - - def reset_device( - self, reset_mode: ResetMode = ResetMode.RST_DEF - ) -> HarpMessage | None: - """ - Resets the device and reboots with all the registers with the default values. Beware that the EEPROM will be erased. More information on the reset device register can be found [here](https://harp-tech.org/protocol/Device.html#r_reset_dev-u8--reset-device-and-save-non-volatile-registers). - - Returns - ------- - HarpMessage - The reply to the Harp message - """ - address = CommonRegisters.RESET_DEV - reply = self.send( - HarpMessage(MessageType.WRITE, PayloadType.U8, address, reset_mode) - ) - - return reply - - def set_clock_config(self, clock_config: ClockConfig) -> HarpMessage | None: - """ - Sets the clock configuration of the device. - - Parameters - ---------- - clock_config : ClockConfig - The clock configuration value - - Returns - ------- - HarpMessage - The reply to the Harp message - """ - address = CommonRegisters.CLOCK_CONFIG - reply = self.send( - HarpMessage(MessageType.WRITE, PayloadType.U8, address, clock_config) - ) - - return reply - - def set_timestamp_offset(self, timestamp_offset: int) -> HarpMessage | None: - """ - When the value of this register is above 0 (zero), the device's timestamp will be offset by this amount. The register is sensitive to 500 microsecond increments. This register is non-volatile. - - Parameters - ---------- - timestamp_offset : int - The timestamp offset value - - Returns - ------- - HarpMessage - The reply to the Harp message - """ - address = CommonRegisters.TIMESTAMP_OFFSET - reply = self.send( - HarpMessage(MessageType.WRITE, PayloadType.U8, address, timestamp_offset) - ) - - return reply - - def send( - self, - message: HarpMessage, - *, - expect_reply: bool = True, - timeout_strategy: TimeoutStrategy | None = None, - ) -> HarpMessage | None: - """ - Sends a Harp message and (optionally) waits for a reply. - - Parameters - ---------- - message : HarpMessage - The HarpMessage to be sent to the device - expect_reply : bool, optional - If False, do not wait for a reply (fire-and-forget) - timeout_strategy : TimeoutStrategy | None - Override the device-level timeout strategy for this call - - Returns - ------- - HarpMessage | None - Reply (or None when allowed by the timeout strategy or expect_reply=False) - - Raises - ------- - HarpTimeoutException - If no reply is received and the effective strategy requires raising - """ - self._ser.write(message.frame) - - if not expect_reply: - return None - - strategy = timeout_strategy or self._timeout_strategy - - reply = self._read() - if reply is None: - hte = HarpTimeoutException(self._timeout, message) - if strategy in ( - TimeoutStrategy.LOG_AND_RAISE, - TimeoutStrategy.LOG_AND_NONE, - ): - self.log.warning(str(hte)) - if strategy in (TimeoutStrategy.RAISE, TimeoutStrategy.LOG_AND_RAISE): - raise hte - else: - self._dump_reply(reply.frame) - return reply - - def _read(self) -> HarpMessage | None: - """ - Reads an incoming serial message in a blocking way. - - Returns - ------- - HarpMessage | None - The incoming Harp message in case it exists - - Raises - ------- - TimeoutError - If no reply is received within the timeout period - """ - try: - return self._ser.msg_q.get(block=True, timeout=self._timeout) - except queue.Empty: - return None - - def _dump_reply(self, reply: bytearray): - """ - Dumps the reply to a Harp message in the dump file in case it exists. - """ - if self._dump_file: - self._dump_file.write(reply) - - def get_events(self) -> list[HarpMessage]: - """ - Gets all events from the event queue. - - Returns - ------- - list - The list containing every Harp event message that were on the queue - """ - msgs = [] - while True: - try: - msg = self._ser.event_q.get(timeout=False) - self._dump_reply(msg.frame) - msgs.append(msg) - except queue.Empty: - break - return msgs - - def event_count(self) -> int: - """ - Gets the number of events in the event queue. - - Returns - ------- - int - The number of events in the event queue - """ - return self._ser.event_q.qsize() - - def read_u8(self, address: int) -> HarpMessage | None: - """ - Reads the value of a register of type U8. - - Parameters - ---------- - address : int - The register to be read - - Returns - ------- - HarpMessage - The reply to the Harp message that will contain the value read from the register - - Raises - ------ - HarpTimeoutException - If no reply is received and the effective strategy requires raising - """ - reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) - - return reply - - def read_s8(self, address: int) -> HarpMessage | None: - """ - Reads the value of a register of type S8. - - Parameters - ---------- - address : int - The register to be read - - Returns - ------- - HarpMessage - The reply to the Harp message that will contain the value read from the register - - Raises - ------ - HarpTimeoutException - If no reply is received and the effective strategy requires raising - """ - reply = self.send(HarpMessage(MessageType.READ, PayloadType.S8, address)) - - return reply - - def read_u16(self, address: int) -> HarpMessage | None: - """ - Reads the value of a register of type U16. - - Parameters - ---------- - address : int - The register to be read - - Returns - ------- - HarpMessage - The reply to the Harp message that will contain the value read from the register - - Raises - ------ - HarpTimeoutException - If no reply is received and the effective strategy requires raising - """ - reply = self.send(HarpMessage(MessageType.READ, PayloadType.U16, address)) - - return reply - - def read_s16(self, address: int) -> HarpMessage | None: - """ - Reads the value of a register of type S16. - - Parameters - ---------- - address : int - The register to be read - - Returns - ------- - HarpMessage - The reply to the Harp message that will contain the value read from the register - - Raises - ------ - HarpTimeoutException - If no reply is received and the effective strategy requires raising - """ - reply = self.send(HarpMessage(MessageType.READ, PayloadType.S16, address)) - - return reply - - def read_u32(self, address: int) -> HarpMessage | None: - """ - Reads the value of a register of type U32. - - Parameters - ---------- - address : int - The register to be read - - Returns - ------- - HarpMessage - The reply to the Harp message that will contain the value read from the register - - Raises - ------ - HarpTimeoutException - If no reply is received and the effective strategy requires raising - """ - reply = self.send(HarpMessage(MessageType.READ, PayloadType.U32, address)) - - return reply - - def read_s32(self, address: int) -> HarpMessage | None: - """ - Reads the value of a register of type S32. - - Parameters - ---------- - address : int - The register to be read - - Returns - ------- - HarpMessage - The reply to the Harp message that will contain the value read from the register - - Raises - ------ - HarpTimeoutException - If no reply is received and the effective strategy requires raising - """ - reply = self.send(HarpMessage(MessageType.READ, PayloadType.S32, address)) - - return reply - - def read_u64(self, address: int) -> HarpMessage | None: - """ - Reads the value of a register of type U64. - - Parameters - ---------- - address : int - The register to be read - - Returns - ------- - HarpMessage - The reply to the Harp message that will contain the value read from the register - - Raises - ------ - HarpTimeoutException - If no reply is received and the effective strategy requires raising - """ - reply = self.send(HarpMessage(MessageType.READ, PayloadType.U64, address)) - - return reply - - def read_s64(self, address: int) -> HarpMessage | None: - """ - Reads the value of a register of type S64. - - Parameters - ---------- - address : int - The register to be read - - Returns - ------- - HarpMessage - The reply to the Harp message that will contain the value read from the register - - Raises - ------ - HarpTimeoutException - If no reply is received and the effective strategy requires raising - """ - reply = self.send(HarpMessage(MessageType.READ, PayloadType.S64, address)) - - return reply - - def read_float(self, address: int) -> HarpMessage | None: - """ - Reads the value of a register of type Float. - - Parameters - ---------- - address : int - The register to be read - - Returns - ------- - HarpMessage - The reply to the Harp message that will contain the value read from the register - - Raises - ------ - HarpTimeoutException - If no reply is received and the effective strategy requires raising - """ - reply = self.send(HarpMessage(MessageType.READ, PayloadType.FLOAT, address)) - - return reply - - def write_u8(self, address: int, value: int | list[int]) -> HarpMessage | None: - """ - Writes the value of a register of type U8. - - Parameters - ---------- - address : int - The register to be written on - value: int | list[int] - The value to be written to the register - - Returns - ------- - HarpMessage - The reply to the Harp message - - Raises - ------ - HarpTimeoutException - If no reply is received and the effective strategy requires raising - """ - reply = self.send( - HarpMessage(MessageType.WRITE, PayloadType.U8, address, value) - ) - - return reply - - def write_s8(self, address: int, value: int | list[int]) -> HarpMessage | None: - """ - Writes the value of a register of type S8. - - Parameters - ---------- - address : int - The register to be written on - value: int | list[int] - The value to be written to the register - - Returns - ------- - HarpMessage - The reply to the Harp message - - Raises - ------ - HarpTimeoutException - If no reply is received and the effective strategy requires raising - """ - reply = self.send( - HarpMessage(MessageType.WRITE, PayloadType.S8, address, value) - ) - - return reply - - def write_u16(self, address: int, value: int | list[int]) -> HarpMessage | None: - """ - Writes the value of a register of type U16. - - Parameters - ---------- - address : int - The register to be written on - value: int | list[int] - The value to be written to the register - - Returns - ------- - HarpMessage - The reply to the Harp message - - Raises - ------ - HarpTimeoutException - If no reply is received and the effective strategy requires raising - """ - reply = self.send( - HarpMessage(MessageType.WRITE, PayloadType.U16, address, value) - ) - - return reply - - def write_s16(self, address: int, value: int | list[int]) -> HarpMessage | None: - """ - Writes the value of a register of type S16. - - Parameters - ---------- - address : int - The register to be written on - value: int | list[int] - The value to be written to the register - - Returns - ------- - HarpMessage - The reply to the Harp message - - Raises - ------ - HarpTimeoutException - If no reply is received and the effective strategy requires raising - """ - reply = self.send( - HarpMessage(MessageType.WRITE, PayloadType.S16, address, value) - ) - - return reply - - def write_u32(self, address: int, value: int | list[int]) -> HarpMessage | None: - """ - Writes the value of a register of type U32. - - Parameters - ---------- - address : int - The register to be written on - value: int | list[int] - The value to be written to the register - - Returns - ------- - HarpMessage - The reply to the Harp message - - Raises - ------ - HarpTimeoutException - If no reply is received and the effective strategy requires raising - """ - reply = self.send( - HarpMessage(MessageType.WRITE, PayloadType.U32, address, value) - ) - - return reply - - def write_s32(self, address: int, value: int | list[int]) -> HarpMessage | None: - """ - Writes the value of a register of type S32. - - Parameters - ---------- - address : int - The register to be written on - value: int | list[int] - The value to be written to the register - - Returns - ------- - HarpMessage - The reply to the Harp message - - Raises - ------ - HarpTimeoutException - If no reply is received and the effective strategy requires raising - """ - reply = self.send( - HarpMessage(MessageType.WRITE, PayloadType.S32, address, value) - ) - - return reply - - def write_u64(self, address: int, value: int | list[int]) -> HarpMessage | None: - """ - Writes the value of a register of type U64. - - Parameters - ---------- - address : int - The register to be written on - value: int | list[int] - The value to be written to the register - - Returns - ------- - HarpMessage - The reply to the Harp message - - Raises - ------ - HarpTimeoutException - If no reply is received and the effective strategy requires raising - """ - reply = self.send( - HarpMessage(MessageType.WRITE, PayloadType.U64, address, value) - ) - - return reply - - def write_s64(self, address: int, value: int | list[int]) -> HarpMessage | None: - """ - Writes the value of a register of type S64. - - Parameters - ---------- - address : int - The register to be written on - value: int | list[int] - The value to be written to the register - - Returns - ------- - HarpMessage - The reply to the Harp message - - Raises - ------ - HarpTimeoutException - If no reply is received and the effective strategy requires raising - """ - reply = self.send( - HarpMessage(MessageType.WRITE, PayloadType.S64, address, value) - ) - - return reply - - def write_float( - self, address: int, value: float | list[float] - ) -> HarpMessage | None: - """ - Writes the value of a register of type Float. - - Parameters - ---------- - address : int - The register to be written on - value: int | list[int] - The value to be written to the register - - Returns - ------- - HarpMessage - The reply to the Harp message - - Raises - ------ - HarpTimeoutException - If no reply is received and the effective strategy requires raising - """ - reply = self.send( - HarpMessage(MessageType.WRITE, PayloadType.FLOAT, address, value) - ) - - return reply - - def _read_who_am_i(self) -> int: - """ - Reads the value stored in the `WHO_AM_I` register. - - Returns - ------- - int - The value of the `WHO_AM_I` register - """ - address = CommonRegisters.WHO_AM_I - - # Attempt to read the WHO_AM_I register to verify if the device is a Harp device - error_msg = "This is not a Harp device." - try: - reply = self.send(HarpMessage(MessageType.READ, PayloadType.U16, address)) - except HarpTimeoutException: - raise HarpException(error_msg) - - if reply is None: - raise HarpException(error_msg) - - return reply.payload - - def _read_hw_version_h(self) -> int: - """ - Reads the value stored in the `HW_VERSION_H` register. - - Returns - ------- - int - The value of the `HW_VERSION_H` register - """ - address = CommonRegisters.HW_VERSION_H - - reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) - - return reply.payload - - def _read_hw_version_l(self) -> int: - """ - Reads the value stored in the `HW_VERSION_L` register. - - Returns - ------- - int - The value of the `HW_VERSION_L` register - """ - address = CommonRegisters.HW_VERSION_L - - reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) - - return reply.payload - - def _read_assembly_version(self) -> int: - """ - Reads the value stored in the `ASSEMBLY_VERSION` register. - - Returns - ------- - int - The value of the `ASSEMBLY_VERSION` register - """ - address = CommonRegisters.ASSEMBLY_VERSION - - reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) - - return reply.payload - - def _read_core_version_h(self) -> int: - """ - Reads the value stored in the `CORE_VERSION_H` register. - - Returns - ------- - int - The value of the `CORE_VERSION_H` register - """ - address = CommonRegisters.CORE_VERSION_H - - reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) - - return reply.payload - - def _read_core_version_l(self) -> int: - """ - Reads the value stored in the `CORE_VERSION_L` register. - - Returns - ------- - int - The value of the `CORE_VERSION_L` register - """ - address = CommonRegisters.CORE_VERSION_L - - reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) - - return reply.payload - - def _read_fw_version_h(self) -> int: - """ - Reads the value stored in the `FW_VERSION_H` register. - - Returns - ------- - int - The value of the `FW_VERSION_H` register - """ - address = CommonRegisters.FIRMWARE_VERSION_H - - reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) - - return reply.payload - - def _read_fw_version_l(self) -> int: - """ - Reads the value stored in the `FW_VERSION_L` register. - - Returns - ------- - int - The value of the `FW_VERSION_L` register - """ - address = CommonRegisters.FIRMWARE_VERSION_L - - reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) - - return reply.payload - - def _read_device_name(self) -> str: - """ - Reads the value stored in the `DEVICE_NAME` register. - - Returns - ------- - int - The value of the `DEVICE_NAME` register - """ - address = CommonRegisters.DEVICE_NAME - - reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) - - return reply.payload_as_string() - - def _read_serial_number(self) -> int: - """ - Reads the value stored in the `SERIAL_NUMBER` register. - - Returns - ------- - int - The value of the `SERIAL_NUMBER` register - """ - address = CommonRegisters.SERIAL_NUMBER - - reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) - - if reply is not None and reply.is_error: - return 0 - - return reply.payload - - def _read_clock_config(self) -> int: - """ - Reads the value stored in the `CLOCK_CONFIG` register. - - Returns - ------- - int - The value of the `CLOCK_CONFIG` register - """ - address = CommonRegisters.CLOCK_CONFIG - - reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) - - return reply.payload - - def _read_timestamp_offset(self) -> int: - """ - Reads the value stored in the `TIMESTAMP_OFFSET` register. - - Returns - ------- - int - The value of the `TIMESTAMP_OFFSET` register - """ - address = CommonRegisters.TIMESTAMP_OFFSET - - reply = self.send(HarpMessage(MessageType.READ, PayloadType.U8, address)) - - return reply.payload - - def __enter__(self): - """ - Support for using Device with 'with' statement. - - Returns - ------- - Device - The Device instance - """ - # Connection is already established in __init__ - # but we could add additional setup if needed - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - """ - Cleanup resources when exiting the 'with' block. - - Parameters - ---------- - exc_type : Exception type or None - Type of the exception that caused the context to be exited - exc_val : Exception or None - Exception instance that caused the context to be exited - exc_tb : traceback or None - Traceback if an exception occurred - """ - self.disconnect() - # Return False to propagate exceptions that occurred in the with block - return False diff --git a/src/harp-serial/harp/serial/harp_serial.py b/src/harp-serial/harp/serial/harp_serial.py deleted file mode 100644 index 00c9593..0000000 --- a/src/harp-serial/harp/serial/harp_serial.py +++ /dev/null @@ -1,160 +0,0 @@ -import logging -import queue -import threading -from functools import partial -from typing import Union - -import serial -import serial.threaded -from harp.protocol.exceptions import HarpException -from harp.protocol.messages import HarpMessage, MessageType - - -class HarpSerialProtocol(serial.threaded.Protocol): - """ - The `HarpSerialProtocol` class deals with the data received from the serial communication. - """ - - _read_q: queue.Queue - - def __init__(self, read_q: queue.Queue, *args, **kwargs): - """ - Parameters - ---------- - read_q : queue.Queue - The queue to where the data received will be put - """ - self._read_q = read_q - self._buffer = bytearray() - super().__init__(*args, **kwargs) - - def connection_made(self, transport: serial.threaded.ReaderThread) -> None: - """ - _TODO_ - - Parameters - ---------- - transport : serial.threaded.ReaderThread - _TODO_ - """ - return super().connection_made(transport) - - def data_received(self, data: bytes) -> None: - """ - Receives data from the serial commmunication. - - Parameters - ---------- - data : bytes - The data received from the serial communication - """ - self._buffer.extend(data) - while True: - if len(self._buffer) < 2: - # not enough data to read the message type and length - break - - # Read length (we can ignore the message type) - message_length = self._buffer[1] - total_length = 2 + message_length - if len(self._buffer) < total_length: - break - - frame = self._buffer[:total_length] - self._buffer = self._buffer[total_length:] - self._read_q.put(frame) - - def connection_lost(self, exc: Union[BaseException, None]) -> None: - """ - _TODO_ - - Parameters - ---------- - exc : exc: Union[BaseException, None] - _TODO_ - """ - return super().connection_lost(exc) - - -class HarpSerial: - """ - The `HarpSerial` deals with the received Harp messages and separates the events from the remaining messages. - - Attributes - ---------- - msg_q : queue.Queue - The queue containing the Harp messages that are not of the type `MessageType.EVENT` - event_q : queue.Queue - The queue containing the Harp messages of `MessageType.EVENT` - """ - - msg_q: queue.Queue - event_q: queue.Queue - - def __init__(self, serial_port: str, **kwargs): - """ - Parameters - ---------- - serial_port : str - The serial port used to establish the connection with the Harp device. It must be denoted as `/dev/ttyUSBx` in Linux and `COMx` in Windows, where `x` is the number of the serial port - """ - ser_kwargs = dict(kwargs) - ser_kwargs.setdefault("exclusive", True) - # Connect to the Harp device - try: - self._ser = serial.Serial(serial_port, **ser_kwargs) - except serial.serialutil.SerialException: - raise HarpException( - f"Error connecting to device. Resource might be busy or without proper permissions: {serial_port}" - ) - - self.log = logging.getLogger(f"{__name__}.{self.__class__.__name__}") - - # Initialize the message queues - self._read_q = queue.Queue() - self.msg_q = queue.Queue() - self.event_q = queue.Queue() - - # Start the thread with the `HarpSerialProtocol` - self._reader = serial.threaded.ReaderThread( - self._ser, - partial(HarpSerialProtocol, self._read_q), - ) - self._reader.start() - self._reader.connect() - - # Start the thread that parses and separates the events from the remaining messages - self._parse_thread = threading.Thread( - target=self.parse_harp_msgs_threaded_buffered, - daemon=True, - ) - self._parse_thread.start() - - def close(self): - """ - Closes the serial port. - """ - self._reader.close() - - def write(self, data): - """ - Writes data to the Harp device. - """ - self._reader.write(data) - - def parse_harp_msgs_threaded_buffered(self): - """ - Parses the Harp messages and separates the events from the remaining messages. - """ - while True: - frame = self._read_q.get() - try: - # Parses the bytearray into a ReplyHarpMessage object - msg = HarpMessage.parse(frame) - if msg.message_type == MessageType.EVENT: - self.event_q.put(msg) - else: - self.msg_q.put(msg) - except Exception as e: - self.log.error(f"Error parsing message: {e}") - self.log.debug(f"Raw data: {frame}") diff --git a/src/harp-serial/pyproject.toml b/src/harp-serial/pyproject.toml deleted file mode 100644 index 6f311aa..0000000 --- a/src/harp-serial/pyproject.toml +++ /dev/null @@ -1,23 +0,0 @@ -[project] -name = "harp-serial" -version = "0.4.0" -description = "Library for data acquisition and control of devices implementing the Harp protocol." -authors = [{ name= "Hardware and Software Platform, Champalimaud Foundation", email="software@research.fchampalimaud.org"}] -license = "MIT" -keywords = ['python', 'harp'] -requires-python = ">=3.10,<4.0" -dependencies = [ - "harp-protocol==0.4.0", - "pyserial>=3.5", -] - -[tool.uv.sources] -harp-protocol = { workspace = true } - -[build-system] -requires = ["uv_build>=0.9.5,<0.10.0"] -build-backend = "uv_build" - -[tool.uv.build-backend] -module-name = "harp.serial" -module-root = "" From 46620503a9d56000748a703d4bb29fdf97661e47 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sun, 26 Apr 2026 15:55:31 -0700 Subject: [PATCH 184/267] Refactor protocol and register api --- pyproject.toml | 28 +- src/harp-protocol/harp/protocol/__init__.py | 119 ++++- src/harp-protocol/harp/protocol/_builder.py | 33 ++ src/harp-protocol/harp/protocol/_checksum.py | 15 + src/harp-protocol/harp/protocol/_framer.py | 105 ++++ src/harp-protocol/harp/protocol/_message.py | 157 ++++++ .../harp/protocol/_message_type.py | 27 + src/harp-protocol/harp/protocol/_payload.py | 188 +++++++ .../harp/protocol/_payload_type.py | 88 ++++ src/harp-protocol/harp/protocol/_register.py | 278 +++++++++++ src/harp-protocol/harp/protocol/base.py | 350 ------------- src/harp-protocol/harp/protocol/exceptions.py | 80 --- src/harp-protocol/harp/protocol/messages.py | 471 ------------------ src/harp-protocol/harp/protocol/py.typed | 0 uv.lock | 120 ++--- 15 files changed, 1074 insertions(+), 985 deletions(-) create mode 100644 src/harp-protocol/harp/protocol/_builder.py create mode 100644 src/harp-protocol/harp/protocol/_checksum.py create mode 100644 src/harp-protocol/harp/protocol/_framer.py create mode 100644 src/harp-protocol/harp/protocol/_message.py create mode 100644 src/harp-protocol/harp/protocol/_message_type.py create mode 100644 src/harp-protocol/harp/protocol/_payload.py create mode 100644 src/harp-protocol/harp/protocol/_payload_type.py create mode 100644 src/harp-protocol/harp/protocol/_register.py delete mode 100644 src/harp-protocol/harp/protocol/base.py delete mode 100644 src/harp-protocol/harp/protocol/exceptions.py delete mode 100644 src/harp-protocol/harp/protocol/messages.py create mode 100644 src/harp-protocol/harp/protocol/py.typed diff --git a/pyproject.toml b/pyproject.toml index 9810daf..02f0a7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ keywords = ['python', 'harp'] requires-python = ">=3.10,<4.0" dependencies = [ "harp-protocol", - "harp-serial", + "harp-device", ] [project.urls] @@ -19,7 +19,7 @@ Documentation = "https://fchampalimaud.github.io/pyharp/" [tool.uv.sources] harp-protocol = { workspace = true } -harp-serial = { workspace = true } +harp-device = { workspace = true } [tool.uv.workspace] members = [ @@ -43,6 +43,10 @@ dev = [ "pytest>=8.3.5", "pytest-cov>=6.1.1", "ruff>=0.11.0", + "ty>=0.0.0", + "harp-protocol", + "harp-device", + "typing-extensions>=4.15.0", ] [tool.ruff.lint.pydocstyle] @@ -56,8 +60,18 @@ python_files = [ "test_*.py" ] -[[tool.uv.index]] -name = "testpypi" -url = "https://test.pypi.org/simple/" -publish-url = "https://test.pypi.org/legacy/" -explicit = true + +[tool.ruff] +line-length = 100 +target-version = "py311" + +[tool.ty.environment] +python-version = "3.11" +extra-paths = ["tests"] + +[tool.ty.src] +include = [ + "harp-protocol/src", + "harp-device/src", + "tests", +] \ No newline at end of file diff --git a/src/harp-protocol/harp/protocol/__init__.py b/src/harp-protocol/harp/protocol/__init__.py index 12e69f4..1fa83d5 100644 --- a/src/harp-protocol/harp/protocol/__init__.py +++ b/src/harp-protocol/harp/protocol/__init__.py @@ -1 +1,118 @@ -from .base import * # noqa: F403 +from ._builder import build_message_frame +from ._checksum import compute as compute_checksum, validate as validate_checksum +from ._framer import HarpFramer +from ._message import HarpMessage, HarpParseError, ParsedHarpMessage, parse as parse_message +from ._message_type import ( + MessageType, + from_byte as message_type_from_byte, + to_byte as message_type_to_byte, +) +from ._payload import ( + PayloadBase, + PayloadU8, + PayloadU16, + PayloadU32, + PayloadU64, + PayloadS8, + PayloadS16, + PayloadS32, + PayloadS64, + PayloadFloat, + PayloadU8Array, + PayloadU16Array, + PayloadU32Array, + PayloadU64Array, + PayloadS8Array, + PayloadS16Array, + PayloadS32Array, + PayloadS64Array, + PayloadFloatArray, +) +from ._payload_type import PayloadType, PayloadTypeInfo, decode_payload_type, encode_payload_type +from ._register import ( + RegisterBase, + RegisterU8, + RegisterU16, + RegisterU32, + RegisterU64, + RegisterS8, + RegisterS16, + RegisterS32, + RegisterS64, + RegisterFloat, + RegisterU8Array, + RegisterU16Array, + RegisterU32Array, + RegisterU64Array, + RegisterS8Array, + RegisterS16Array, + RegisterS32Array, + RegisterS64Array, + RegisterFloatArray, +) + +__all__ = [ + # Message type + "MessageType", + "message_type_from_byte", + "message_type_to_byte", + # Payload type + "PayloadType", + "PayloadTypeInfo", + "decode_payload_type", + "encode_payload_type", + # Checksum + "compute_checksum", + "validate_checksum", + # Message + "HarpMessage", + "ParsedHarpMessage", + "HarpParseError", + "parse_message", + # Framer + "HarpFramer", + # Payload DSL + "PayloadBase", + "PayloadU8", + "PayloadU16", + "PayloadU32", + "PayloadU64", + "PayloadS8", + "PayloadS16", + "PayloadS32", + "PayloadS64", + "PayloadFloat", + "PayloadU8Array", + "PayloadU16Array", + "PayloadU32Array", + "PayloadU64Array", + "PayloadS8Array", + "PayloadS16Array", + "PayloadS32Array", + "PayloadS64Array", + "PayloadFloatArray", + # Register DSL + "RegisterBase", + # Register scalar types + "RegisterU8", + "RegisterU16", + "RegisterU32", + "RegisterU64", + "RegisterS8", + "RegisterS16", + "RegisterS32", + "RegisterS64", + "RegisterFloat", + # Register array types + "RegisterU8Array", + "RegisterU16Array", + "RegisterU32Array", + "RegisterU64Array", + "RegisterS8Array", + "RegisterS16Array", + "RegisterS32Array", + "RegisterS64Array", + "RegisterFloatArray", + # Frame builders + "build_message_frame", +] diff --git a/src/harp-protocol/harp/protocol/_builder.py b/src/harp-protocol/harp/protocol/_builder.py new file mode 100644 index 0000000..db04109 --- /dev/null +++ b/src/harp-protocol/harp/protocol/_builder.py @@ -0,0 +1,33 @@ +"""Utilities for building outgoing Harp message frames.""" + +import struct + +from ._message_type import MessageType +from ._message_type import to_byte as _msg_type_byte +from ._payload_type import PayloadType, encode_payload_type + + +def build_message_frame( + message_type: MessageType, + address: int, + payload_type: PayloadType, + payload: bytes = b"", + *, + port: int = 0xFF, + timestamp: float | None = None, +) -> bytes: + """Build and return a complete Harp wire frame as bytes.""" + if timestamp is not None: + seconds = int(timestamp) + microseconds = round((timestamp - seconds) / 32e-6) + ts_bytes = struct.pack(" int: + """Wrapping u8 sum of all bytes except the last (the checksum byte itself).""" + total = 0 + mv = memoryview(bytes(data)) if not isinstance(data, (bytes, bytearray, memoryview)) else data + for b in mv[:-1]: + total = (total + b) & 0xFF + return total + + +def validate(data: bytes | bytearray | memoryview) -> bool: + """Return True if the last byte equals the checksum of all preceding bytes.""" + mv = memoryview(bytes(data)) if not isinstance(data, (bytes, bytearray, memoryview)) else data + if len(mv) < 2: + return False + return compute(mv) == mv[-1] diff --git a/src/harp-protocol/harp/protocol/_framer.py b/src/harp-protocol/harp/protocol/_framer.py new file mode 100644 index 0000000..c02a79d --- /dev/null +++ b/src/harp-protocol/harp/protocol/_framer.py @@ -0,0 +1,105 @@ +from collections.abc import Iterator +from pathlib import Path + +from ._message import HarpMessage, HarpParseError, parse +from ._message_type import from_byte as _validate_message_type + + +class HarpFramer: + """Stateful Harp message stream parser. + + Feed raw bytes incrementally with feed(), then drain complete frames with + next_frame() or by iterating. Suitable for both file parsing and streaming + sources (e.g. serial ports) where data arrives in chunks. + + Recovery: on checksum or PayloadType failure, the framer skips exactly the + bad MessageType byte and retries from the next byte — matching the C# + StreamTransport resynchronisation strategy. + """ + + def __init__(self) -> None: + self._buf: bytearray = bytearray() + self._pos: int = 0 + + # ------------------------------------------------------------------ + # Public API + # ------------------------------------------------------------------ + + def feed(self, data: bytes | bytearray) -> None: + """Append new bytes to the internal buffer.""" + self._buf.extend(data) + # Compact once we've consumed a decent chunk to avoid unbounded growth. + if self._pos > 4096: + del self._buf[: self._pos] + self._pos = 0 + + def next_frame(self) -> HarpMessage | None: + """Return the next complete, valid HarpMessage, or None if not enough data.""" + buf = self._buf + pos = self._pos + + while pos < len(buf): + # ── State 1: Seek ────────────────────────────────────────────── + # Find a byte that looks like a valid MessageType. + try: + _validate_message_type(buf[pos]) + except ValueError: + pos += 1 + continue + + msg_type_pos = pos + + # ── State 2: ReadLength ──────────────────────────────────────── + if pos + 1 >= len(buf): + break # need more data + + length = buf[pos + 1] + if length == 0: + # Length=0 is invalid (no remaining bytes, not even checksum). + pos += 1 + continue + + # ── State 3: ReadBody ────────────────────────────────────────── + frame_end = pos + 2 + length + if frame_end > len(buf): + break # frame not yet complete + + frame = bytes(buf[pos:frame_end]) + try: + msg = parse(frame) + pos = frame_end + self._pos = pos + return msg + except HarpParseError: + # Recovery: skip the bad MessageType byte, retry from pos+1. + pos = msg_type_pos + 1 + continue + + self._pos = pos + return None + + def frames(self) -> Iterator[HarpMessage]: + """Yield all complete frames currently available in the buffer.""" + while (msg := self.next_frame()) is not None: + yield msg + + def __iter__(self) -> Iterator[HarpMessage]: + return self.frames() + + # ------------------------------------------------------------------ + # Convenience class methods + # ------------------------------------------------------------------ + + @classmethod + def parse_bytes(cls, data: bytes | bytearray) -> list[HarpMessage]: + """Parse all Harp messages from a byte buffer.""" + framer = cls() + framer.feed(data) + return list(framer.frames()) + + @classmethod + def parse_file(cls, path: str | Path) -> list[HarpMessage]: + """Parse all Harp messages from a binary file.""" + with open(path, "rb") as f: + data = f.read() + return cls.parse_bytes(data) diff --git a/src/harp-protocol/harp/protocol/_message.py b/src/harp-protocol/harp/protocol/_message.py new file mode 100644 index 0000000..70959a0 --- /dev/null +++ b/src/harp-protocol/harp/protocol/_message.py @@ -0,0 +1,157 @@ +"""Harp message container.""" + +from __future__ import annotations + +import struct +from typing import Any, Generic, TypeVar + +from ._builder import build_message_frame +from ._checksum import validate as _validate_checksum +from ._message_type import MessageType +from ._payload import PayloadBase +from ._payload_type import PayloadType, decode_payload_type + +P = TypeVar("P", bound=PayloadBase[Any]) + + +class HarpParseError(Exception): + pass + + +class HarpMessage: + """A Harp message backed by its raw frame bytes. + + Build with the constructor or parse from wire bytes with ``HarpMessage.parse()``. + """ + + __slots__ = ("_frame",) + + def __init__( + self, + message_type: MessageType, + address: int, + payload_type: PayloadType, + payload: bytes = b"", + *, + port: int = 0xFF, + timestamp: float | None = None, + ) -> None: + self._frame: bytes = build_message_frame( + message_type, address, payload_type, payload, port=port, timestamp=timestamp + ) + + @classmethod + def parse(cls, data: bytes | bytearray | memoryview) -> "HarpMessage": + """Parse and validate a complete Harp frame. Raises ``HarpParseError`` on failure.""" + raw = data if isinstance(data, bytes) else bytes(data) + + if len(raw) < 6: + raise HarpParseError(f"Frame too short: {len(raw)} bytes (minimum 6)") + + if not _validate_checksum(raw): + raise HarpParseError("Checksum mismatch") + + # Validate MessageType byte (bits 7,6,5,4,2 must be 0; bits 1:0 are type) + b0 = raw[0] + if b0 & 0b11110100: + raise HarpParseError(f"Reserved bits set in MessageType byte: 0x{b0:02x}") + if (b0 & 0x03) not in (1, 2, 3): + raise HarpParseError(f"Invalid MessageType value in byte: 0x{b0:02x}") + + length = raw[1] + if len(raw) != length + 2: + raise HarpParseError(f"Length field {length} inconsistent with buffer size {len(raw)}") + + try: + decode_payload_type(raw[4]) + except ValueError as exc: + raise HarpParseError(str(exc)) from exc + + if bool(raw[4] & 0x10) and len(raw) < 5 + 6 + 1: + raise HarpParseError("Frame too short to contain timestamp") + + obj = cls.__new__(cls) + obj._frame = raw + return obj + + # ------------------------------------------------------------------ + # Field accessors — direct bit reads into _frame, no copies + # ------------------------------------------------------------------ + + @property + def message_type(self) -> MessageType: + return MessageType(self._frame[0] & 0x03) + + @property + def has_error(self) -> bool: + return bool(self._frame[0] & 0x08) + + @property + def address(self) -> int: + return self._frame[2] + + @property + def port(self) -> int: + return self._frame[3] + + @property + def payload_type(self) -> PayloadType: + return decode_payload_type(self._frame[4]).payload_type + + @property + def has_timestamp(self) -> bool: + return bool(self._frame[4] & 0x10) + + @property + def timestamp(self) -> float | None: + if not self.has_timestamp: + return None + seconds, microseconds = struct.unpack_from(" memoryview: + """Payload bytes, excluding timestamp and checksum.""" + offset = 11 if self.has_timestamp else 5 + return memoryview(self._frame)[offset:-1] + + def __repr__(self) -> str: + return ( + f"HarpMessage(message_type={self.message_type!r}, address={self.address:#04x}, " + f"payload_type={self.payload_type!r}, timestamp={self.timestamp!r})" + ) + + +class ParsedHarpMessage(HarpMessage, Generic[P]): + """A ``HarpMessage`` with a typed parsed payload attached.""" + + __slots__ = ("parsed",) + + def __init__( + self, + message_type: MessageType, + address: int, + payload_type: PayloadType, + payload: bytes = b"", + *, + port: int = 0xFF, + timestamp: float | None = None, + parsed: P, + ) -> None: + super().__init__( + message_type, address, payload_type, payload, port=port, timestamp=timestamp + ) + self.parsed = parsed + + @classmethod + def from_message(cls, msg: HarpMessage, parsed: P) -> "ParsedHarpMessage[P]": + """Wrap a ``HarpMessage`` with a pre-parsed payload.""" + obj = cls.__new__(cls) + obj._frame = msg._frame + obj.parsed = parsed + return obj + + +def parse(data: bytes | bytearray | memoryview) -> HarpMessage: + """Parse a complete Harp message frame. Alias for ``HarpMessage.parse()``.""" + return HarpMessage.parse(data) diff --git a/src/harp-protocol/harp/protocol/_message_type.py b/src/harp-protocol/harp/protocol/_message_type.py new file mode 100644 index 0000000..324f984 --- /dev/null +++ b/src/harp-protocol/harp/protocol/_message_type.py @@ -0,0 +1,27 @@ +from enum import IntEnum + + +class MessageType(IntEnum): + Read = 1 + Write = 2 + Event = 3 + + +# Bits 7,6,5,4,2 must be 0; bit 3 is error; bits 1:0 are type. +_RESERVED_MASK = 0b11110100 +_VALID_TYPES = frozenset(t.value for t in MessageType) + + +def from_byte(b: int) -> tuple["MessageType", bool]: + """Decode a MessageType byte into ``(MessageType, has_error)``. Raises ``ValueError`` on invalid input.""" + if b & _RESERVED_MASK: + raise ValueError(f"Reserved bits set in MessageType byte: 0x{b:02x}") + type_bits = b & 0x03 + if type_bits not in _VALID_TYPES: + raise ValueError(f"Invalid MessageType value {type_bits} in byte: 0x{b:02x}") + return MessageType(type_bits), bool(b & 0x08) + + +def to_byte(message_type: MessageType, has_error: bool = False) -> int: + """Encode MessageType + error flag to a single byte.""" + return message_type.value | (0x08 if has_error else 0) diff --git a/src/harp-protocol/harp/protocol/_payload.py b/src/harp-protocol/harp/protocol/_payload.py new file mode 100644 index 0000000..8c78238 --- /dev/null +++ b/src/harp-protocol/harp/protocol/_payload.py @@ -0,0 +1,188 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, ClassVar, Generic, Self, TypeVar, final + +import numpy as np +import pandas as pd +from numpy.typing import NDArray + +if TYPE_CHECKING: + from ._payload_type import PayloadType + +ScalarT = TypeVar("ScalarT", bound=np.generic | np.ndarray) + + +class PayloadBase(Generic[ScalarT]): + """Base class for typed Harp register payloads. + + Subclasses define ``_dtype: ClassVar[np.dtype]`` for the register layout:: + + class AnalogDataPayload(PayloadBase): + _dtype: ClassVar = np.dtype([ + ("analog_input0", " None: + """Construct a single-sample payload. Structured dtypes use keyword arguments; scalar dtypes use a single positional argument.""" + if self._dtype.names is not None: + # Structured dtype — keyword-only construction + if args: + raise TypeError( + f"{type(self).__name__}() requires keyword arguments, got positional args" + ) + unknown = set(kwargs) - set(self._dtype.names) + if unknown: + raise TypeError(f"{type(self).__name__}() got unexpected kwargs: {sorted(unknown)}") + values = tuple(kwargs[n] for n in self._dtype.names) + self._arr = np.array([values], dtype=self._dtype) + else: + # Scalar dtype — single positional value + if len(args) != 1 or kwargs: + raise TypeError(f"{type(self).__name__}() takes exactly one positional argument") + self._arr = np.array([args[0]], dtype=self._dtype) + + def __init_subclass__(cls, **kwargs: object) -> None: + super().__init_subclass__(**kwargs) + # Ensure _dtype is a proper np.dtype when defined on the subclass. + if "_dtype" in cls.__dict__: + raw = cls.__dict__["_dtype"] + if not isinstance(raw, np.dtype): + cls._dtype = np.dtype(raw) + + @classmethod + def scalar(cls, payload_type: "PayloadType") -> type[PayloadBase]: + """Return a dynamically-generated ``PayloadBase`` subclass for a scalar type.""" + ## TODO we should prob consider removing this and just require explicit payload classes for all registers, even scalars. It's only a few lines of boilerplate to define a new one, and it would simplify the codebase by eliminating this dynamic class generation logic. + + dtype = payload_type.numpy_dtype + name = f"_Scalar{payload_type.name}Payload" + return type(name, (PayloadBase,), {"_dtype": dtype}) + + @classmethod + def from_buffer(cls, buf: bytes | bytearray | memoryview) -> Self: + """Construct from a raw byte buffer interpreted as an array of ``_dtype`` records.""" + arr = np.frombuffer(buf, dtype=cls._dtype) + obj = cls.__new__(cls) + obj._arr = arr + return obj + + @property + def value(self) -> ScalarT: + """Returns a single scalar if the array has one element, otherwise the full array.""" + if len(self._arr) == 1: + return self._arr[0] # type: ignore[return-value] + return self._arr # type: ignore[return-value] # ty: ignore[invalid-return-type] + + @property + def payload(self) -> NDArray[np.void]: + """Raw structured numpy array.""" + return self._arr + + def to_dataframe(self) -> pd.DataFrame: + """Convert to a DataFrame. Structured dtypes produce one column per field; scalar dtypes produce a ``"value"`` column.""" + if self._dtype.names is not None: + return pd.DataFrame({name: self._arr[name] for name in self._dtype.names}) + return pd.DataFrame({"value": self._arr}) + + def __len__(self) -> int: + return len(self._arr) + + +# ------------------------------------------------------------------ +# Named scalar payload classes — one per PayloadType. +# These are the concrete types returned by RegisterU8, RegisterU16, etc. +# ------------------------------------------------------------------ + + +class PayloadU8(PayloadBase[np.uint8]): + _dtype: ClassVar = np.dtype("u1") + + +class PayloadU16(PayloadBase[np.uint16]): + _dtype: ClassVar = np.dtype(" np.dtype: + return self.value + + +@dataclass(frozen=True) +class PayloadTypeInfo: + has_timestamp: bool + payload_type: PayloadType + element_size: int # bytes per element + + +_SIZE_TO_UNSIGNED = { + 1: PayloadType.U8, + 2: PayloadType.U16, + 4: PayloadType.U32, + 8: PayloadType.U64, +} +_SIZE_TO_SIGNED = { + 1: PayloadType.S8, + 2: PayloadType.S16, + 4: PayloadType.S32, + 8: PayloadType.S64, +} + + +def decode_payload_type(b: int) -> PayloadTypeInfo: + """Decode a PayloadType byte. Raises ``ValueError`` for invalid bytes.""" + size = b & 0x0F + if size not in (1, 2, 4, 8): + raise ValueError(f"Invalid size nibble {size} in PayloadType byte: 0x{b:02x}") + if b & 0x20: + raise ValueError(f"Reserved bit 5 set in PayloadType byte: 0x{b:02x}") + + has_timestamp = bool(b & 0x10) + is_float = bool(b & 0x40) + is_signed = bool(b & 0x80) + + if is_float and is_signed: + raise ValueError(f"IsFloat and IsSigned cannot both be set: 0x{b:02x}") + if is_float and (b & 0xEF) != 0x44: + raise ValueError(f"Float payload must have size=4 only: 0x{b:02x}") + + if is_float: + payload_type = PayloadType.Float + elif is_signed: + payload_type = _SIZE_TO_SIGNED[size] + else: + payload_type = _SIZE_TO_UNSIGNED[size] + + return PayloadTypeInfo( + has_timestamp=has_timestamp, payload_type=payload_type, element_size=size + ) + + +def encode_payload_type(payload_type: PayloadType, *, has_timestamp: bool = False) -> int: + """Encode a PayloadType back to a protocol byte.""" + dtype = payload_type.numpy_dtype + kind = dtype.kind # 'u', 'i', 'f' + size = dtype.itemsize + + b = size + if kind == "i": + b |= 0x80 + elif kind == "f": + b |= 0x40 + if has_timestamp: + b |= 0x10 + return int(b) diff --git a/src/harp-protocol/harp/protocol/_register.py b/src/harp-protocol/harp/protocol/_register.py new file mode 100644 index 0000000..4965bdf --- /dev/null +++ b/src/harp-protocol/harp/protocol/_register.py @@ -0,0 +1,278 @@ +"""Register base classes for the Harp protocol. + +Define a register by subclassing the appropriate typed class:: + + class TimestampSecond(RegisterU32): + address: ClassVar[int] = 8 + + TimestampSecond.format() # → Read request frame + TimestampSecond.format(42) # → Write request frame + TimestampSecond.parse(msg) # → PayloadU32 instance +""" + +from abc import ABC, ABCMeta +from typing import Any, ClassVar, Generic, TypeVar, cast, final, overload + +import numpy as np +from typing_extensions import Sentinel + +from ._builder import build_message_frame +from ._message import HarpMessage +from ._message_type import MessageType +from ._payload import ( + PayloadBase, + PayloadFloat, + PayloadFloatArray, + PayloadS8, + PayloadS8Array, + PayloadS16, + PayloadS16Array, + PayloadS32, + PayloadS32Array, + PayloadS64, + PayloadS64Array, + PayloadU8, + PayloadU8Array, + PayloadU16, + PayloadU16Array, + PayloadU32, + PayloadU32Array, + PayloadU64, + PayloadU64Array, +) +from ._payload_type import PayloadType + +_MISSING = Sentinel("_MISSING") + +P = TypeVar("P", bound=PayloadBase[Any]) +_R = TypeVar("_R") +_AR = TypeVar("_AR", bound="RegisterBase[Any]") # type: ignore[type-arg] + + +class _RegisterMeta(ABCMeta): + """Calling a register class with an address creates a one-off subclass: ``RegisterU32(0x08)``.""" + + def __call__(cls: "type[_R]", address: int) -> "type[_R]": # type: ignore[override] + return cast( + "type[_R]", + type(f"{cls.__name__}_{address:#04x}", (cls,), {"address": address}), + ) + + +class RegisterBase(ABC, Generic[P]): + """Abstract base for all typed Harp registers. + + Subclasses must define ``address`` and ``payload_type`` as ``ClassVar``s. + Optionally define ``payload_class`` for structured payloads; otherwise a scalar one is generated. + """ + + address: ClassVar[int] + payload_type: ClassVar[PayloadType] + payload_class: ClassVar[type[Any]] + length: ClassVar[int | None] = None + + def __init_subclass__(cls, **kwargs: object) -> None: + super().__init_subclass__(**kwargs) + # Auto-generate scalar payload class if not explicitly defined + if "payload_class" not in cls.__dict__ and "payload_type" in cls.__dict__: + cls.payload_class = PayloadBase.scalar(cls.payload_type) + + @classmethod + def parse(cls, value: HarpMessage | bytes | bytearray | memoryview) -> P: # type: ignore[type-var] + """Parse the payload from a ``HarpMessage`` or raw bytes.""" + buf = value.payload if isinstance(value, HarpMessage) else value + return cls.payload_class.from_buffer(buf) + + @classmethod + def parse_bulk(cls, data: bytes | bytearray | memoryview | list[HarpMessage]) -> P: # type: ignore[type-var] + """Parse a batch of payloads from concatenated bytes or a list of messages.""" + if isinstance(data, list): + data = b"".join(msg.payload for msg in data) + return cls.payload_class.from_buffer(data) + + @overload + @classmethod + def format( + cls, + *, + message_type: MessageType = MessageType.Read, + port: int = 0xFF, + ) -> bytes: ... + + @overload + @classmethod + def format( + cls, + value: Any, + *, + message_type: MessageType = MessageType.Write, + port: int = 0xFF, + ) -> bytes: ... + + @final + @classmethod + def format( + cls, + value: Any = _MISSING, + *, + message_type: MessageType | None = None, + port: int = 0xFF, + ) -> bytes: + """Build a Harp frame for this register. No value → Read; with value → Write.""" + if value is _MISSING: + mt = MessageType.Read if message_type is None else message_type + return build_message_frame(mt, cls.address, cls.payload_type, port=port) + else: + mt = MessageType.Write if message_type is None else message_type + if isinstance(value, PayloadBase): + # Payload instance — use its backing array bytes directly + raw = value._arr.tobytes() + elif isinstance(value, np.ndarray) and value.dtype != cls.payload_type.numpy_dtype: + # Structured numpy array passed by hand + raw = value.tobytes() + else: + # Scalar or array castable to the register's primitive dtype + raw = np.asarray(value, dtype=cls.payload_type.numpy_dtype).tobytes() + return build_message_frame(mt, cls.address, cls.payload_type, raw, port=port) + + +# ------------------------------------------------------------------ +# Typed scalar classes — one per PayloadType. +# These can be used both as base classes for named registers: +# +# class TimestampSecond(RegisterU32): +# address: ClassVar[int] = 8 +# +# and as on-the-fly register classes for raw addresses: +# +# dev.read(RegisterU32(0x08)) +# ------------------------------------------------------------------ + + +class RegisterU8(RegisterBase[PayloadU8], metaclass=_RegisterMeta): # type: ignore[misc] + payload_type: ClassVar[PayloadType] = PayloadType.U8 + payload_class: ClassVar[type[PayloadU8]] = PayloadU8 + + +class RegisterU16(RegisterBase[PayloadU16], metaclass=_RegisterMeta): # type: ignore[misc] + payload_type: ClassVar[PayloadType] = PayloadType.U16 + payload_class: ClassVar[type[PayloadU16]] = PayloadU16 + + +class RegisterU32(RegisterBase[PayloadU32], metaclass=_RegisterMeta): # type: ignore[misc] + payload_type: ClassVar[PayloadType] = PayloadType.U32 + payload_class: ClassVar[type[PayloadU32]] = PayloadU32 + + +class RegisterU64(RegisterBase[PayloadU64], metaclass=_RegisterMeta): # type: ignore[misc] + payload_type: ClassVar[PayloadType] = PayloadType.U64 + payload_class: ClassVar[type[PayloadU64]] = PayloadU64 + + +class RegisterS8(RegisterBase[PayloadS8], metaclass=_RegisterMeta): # type: ignore[misc] + payload_type: ClassVar[PayloadType] = PayloadType.S8 + payload_class: ClassVar[type[PayloadS8]] = PayloadS8 + + +class RegisterS16(RegisterBase[PayloadS16], metaclass=_RegisterMeta): # type: ignore[misc] + payload_type: ClassVar[PayloadType] = PayloadType.S16 + payload_class: ClassVar[type[PayloadS16]] = PayloadS16 + + +class RegisterS32(RegisterBase[PayloadS32], metaclass=_RegisterMeta): # type: ignore[misc] + payload_type: ClassVar[PayloadType] = PayloadType.S32 + payload_class: ClassVar[type[PayloadS32]] = PayloadS32 + + +class RegisterS64(RegisterBase[PayloadS64], metaclass=_RegisterMeta): # type: ignore[misc] + payload_type: ClassVar[PayloadType] = PayloadType.S64 + payload_class: ClassVar[type[PayloadS64]] = PayloadS64 + + +class RegisterFloat(RegisterBase[PayloadFloat], metaclass=_RegisterMeta): # type: ignore[misc] + payload_type: ClassVar[PayloadType] = PayloadType.Float + payload_class: ClassVar[type[PayloadFloat]] = PayloadFloat + + +# ------------------------------------------------------------------ +# Array register classes — fixed-length element arrays per message. +# +# Named register usage: +# +# class DigitalOutputs(RegisterU16Array): +# address: ClassVar[int] = 0x28 +# length: ClassVar[int] = 3 +# +# On-the-fly usage: +# +# dev.read(RegisterU16Array(0x28, length=3)) +# ------------------------------------------------------------------ + + +class _ArrayRegisterMeta(ABCMeta): + """Calling with address and length creates a concrete subclass: ``RegisterU16Array(0x28, length=3)``.""" + + def __call__(cls: "type[_AR]", address: int, *, length: int) -> "type[_AR]": # type: ignore[override] + base_payload = cls.payload_class # type: ignore[attr-defined] + concrete_payload = type( + f"{base_payload.__name__}_{length}", + (base_payload,), + {"_dtype": np.dtype((base_payload._dtype, length))}, # type: ignore[attr-defined] + ) + return cast( + "type[_AR]", + type( + f"{cls.__name__}_{address:#04x}", + (cls,), + { + "address": address, + "length": length, + "payload_class": concrete_payload, + }, + ), + ) + + +class RegisterU8Array(RegisterBase[PayloadU8Array], metaclass=_ArrayRegisterMeta): # type: ignore[misc] + payload_type: ClassVar[PayloadType] = PayloadType.U8 + payload_class: ClassVar[type[Any]] = PayloadU8Array + + +class RegisterU16Array(RegisterBase[PayloadU16Array], metaclass=_ArrayRegisterMeta): # type: ignore[misc] + payload_type: ClassVar[PayloadType] = PayloadType.U16 + payload_class: ClassVar[type[Any]] = PayloadU16Array + + +class RegisterU32Array(RegisterBase[PayloadU32Array], metaclass=_ArrayRegisterMeta): # type: ignore[misc] + payload_type: ClassVar[PayloadType] = PayloadType.U32 + payload_class: ClassVar[type[Any]] = PayloadU32Array + + +class RegisterU64Array(RegisterBase[PayloadU64Array], metaclass=_ArrayRegisterMeta): # type: ignore[misc] + payload_type: ClassVar[PayloadType] = PayloadType.U64 + payload_class: ClassVar[type[Any]] = PayloadU64Array + + +class RegisterS8Array(RegisterBase[PayloadS8Array], metaclass=_ArrayRegisterMeta): # type: ignore[misc] + payload_type: ClassVar[PayloadType] = PayloadType.S8 + payload_class: ClassVar[type[Any]] = PayloadS8Array + + +class RegisterS16Array(RegisterBase[PayloadS16Array], metaclass=_ArrayRegisterMeta): # type: ignore[misc] + payload_type: ClassVar[PayloadType] = PayloadType.S16 + payload_class: ClassVar[type[Any]] = PayloadS16Array + + +class RegisterS32Array(RegisterBase[PayloadS32Array], metaclass=_ArrayRegisterMeta): # type: ignore[misc] + payload_type: ClassVar[PayloadType] = PayloadType.S32 + payload_class: ClassVar[type[Any]] = PayloadS32Array + + +class RegisterS64Array(RegisterBase[PayloadS64Array], metaclass=_ArrayRegisterMeta): # type: ignore[misc] + payload_type: ClassVar[PayloadType] = PayloadType.S64 + payload_class: ClassVar[type[Any]] = PayloadS64Array + + +class RegisterFloatArray(RegisterBase[PayloadFloatArray], metaclass=_ArrayRegisterMeta): # type: ignore[misc] + payload_type: ClassVar[PayloadType] = PayloadType.Float + payload_class: ClassVar[type[Any]] = PayloadFloatArray diff --git a/src/harp-protocol/harp/protocol/base.py b/src/harp-protocol/harp/protocol/base.py deleted file mode 100644 index 7419d3c..0000000 --- a/src/harp-protocol/harp/protocol/base.py +++ /dev/null @@ -1,350 +0,0 @@ -from datetime import datetime -from enum import IntEnum, IntFlag - -# The reference epoch for UTC harp time -REFERENCE_EPOCH = datetime(1904, 1, 1) - - -class MessageType(IntEnum): - """ - An enumeration of the allowed message types of a Harp message. More information on the MessageType byte of a Harp message can be found [here](https://harp-tech.org/protocol/BinaryProtocol-8bit.html#messagetype-1-byte). - - Attributes - ---------- - READ : int - The value that corresponds to a Read Harp message (1) - WRITE : int - The value that corresponds to a Write Harp message (2) - EVENT : int - The value that corresponds to an Event Harp message (3). Messages of this type are only meant to be send by the device - READ_ERROR : int - The value that corresponds to a Read Error Harp message (9). Messages of this type are only meant to be send by the device - WRITE_ERROR : int - The value that corresponds to a Write Error Harp message (10). Messages of this type are only meant to be send by the device - """ - - READ = 0x01 - WRITE = 0x02 - EVENT = 0x03 - ERROR = 0x08 - READ_ERROR = READ | ERROR - WRITE_ERROR = WRITE | ERROR - - def is_error(self) -> bool: - """ - Check if the message type is an error message. - Returns - ------- - bool - Returns True if the message type is an error message, False otherwise. - """ - return bool(self & MessageType.ERROR) - - -class _PayloadTypeFlags(IntEnum): - """ - Internal flags used to define the PayloadType enumeration. - - Attributes - ---------- - HAS_TIMESTAMP : int - Flag indicating that the message has a timestamp - IS_FLOAT : int - Flag indicating that the message payload is a float - IS_SIGNED : int - Flag indicating that the message payload is signed - TYPE_SIZE : int - Mask to get the size of the message payload in bytes - """ - - HAS_TIMESTAMP = 0x10 - IS_FLOAT = 0x40 - IS_SIGNED = 0x80 - TYPE_SIZE = 0x0F - - -class PayloadType(IntEnum): - """ - An enumeration of the allowed payload types of a Harp message. More information on the PayloadType byte of a Harp message can be found [here](https://harp-tech.org/protocol/BinaryProtocol-8bit.html#payloadtype-1-byte). - - Attributes - ---------- - U8 : int - The value that corresponds to a message of type U8 - S8 : int - The value that corresponds to a message of type S8 - U16 : int - The value that corresponds to a message of type U16 - S16 : int - The value that corresponds to a message of type S16 - U32 : int - The value that corresponds to a message of type U32 - S32 : int - The value that corresponds to a message of type S32 - U64 : int - The value that corresponds to a message of type U64 - S64 : int - The value that corresponds to a message of type S64 - FLOAT : int - The value that corresponds to a message of type Float - TIMESTAMP : int - The value that corresponds to a message of type Timestamp. This is not a valid PayloadType, but it is used to indicate that the message has a timestamp. - TIMESTAMPED_U8 : int - The value that corresponds to a message of type TimestampedU8 - TIMESTAMPED_S8 : int - The value that corresponds to a message of type TimestampedS8 - TIMESTAMPED_U16 : int - The value that corresponds to a message of type TimestampedU16 - TIMESTAMPED_S16 : int - The value that corresponds to a message of type TimestampedS16 - TIMESTAMPED_U32 : int - The value that corresponds to a message of type TimestampedU32 - TIMESTAMPED_S32 : int - The value that corresponds to a message of type TimestampedS32 - TIMESTAMPED_U64 : int - The value that corresponds to a message of type TimestampedU64 - TIMESTAMPED_S64 : int - The value that corresponds to a message of type TimestampedS64 - TIMESTAMPED_FLOAT : int - The value that corresponds to a message of type TimestampedFloat - """ - - U8 = 0x01 - S8 = _PayloadTypeFlags.IS_SIGNED | 0x01 - U16 = 0x02 - S16 = _PayloadTypeFlags.IS_SIGNED | 0x02 - U32 = 0x04 - S32 = _PayloadTypeFlags.IS_SIGNED | 0x04 - U64 = 0x08 - S64 = _PayloadTypeFlags.IS_SIGNED | 0x08 - FLOAT = _PayloadTypeFlags.IS_FLOAT | 0x04 - TIMESTAMPED_U8 = _PayloadTypeFlags.HAS_TIMESTAMP | U8 - TIMESTAMPED_S8 = _PayloadTypeFlags.HAS_TIMESTAMP | S8 - TIMESTAMPED_U16 = _PayloadTypeFlags.HAS_TIMESTAMP | U16 - TIMESTAMPED_S16 = _PayloadTypeFlags.HAS_TIMESTAMP | S16 - TIMESTAMPED_U32 = _PayloadTypeFlags.HAS_TIMESTAMP | U32 - TIMESTAMPED_S32 = _PayloadTypeFlags.HAS_TIMESTAMP | S32 - TIMESTAMPED_U64 = _PayloadTypeFlags.HAS_TIMESTAMP | U64 - TIMESTAMPED_S64 = _PayloadTypeFlags.HAS_TIMESTAMP | S64 - TIMESTAMPED_FLOAT = _PayloadTypeFlags.HAS_TIMESTAMP | FLOAT - - def has_timestamp(self): - """ - Check if this PayloadType has a timestamp. - - Returns - ------- - bool - Returns True if this PayloadType has a timestamp, False otherwise. - """ - return bool(self & _PayloadTypeFlags.HAS_TIMESTAMP) - - def is_float(self): - """ - Check if this `PayloadType` is a float. - - Returns - ------- - bool - Returns True if this `PayloadType` is a float, False otherwise. - """ - return bool(self & _PayloadTypeFlags.IS_FLOAT) - - def is_signed(self): - """ - Check if this PayloadType is signed. - - Returns - ------- - bool - Returns True if this PayloadType is signed, False otherwise. - """ - return bool(self & _PayloadTypeFlags.IS_SIGNED) - - def type_size(self): - """ - Get the size of this PayloadType in bytes. - - Returns - ------- - int - The size of this PayloadType in bytes. - """ - return self & _PayloadTypeFlags.TYPE_SIZE - - -class CommonRegisters(IntEnum): - """ - An enumeration with the registers that are common to every Harp device. More information on the common registers can be found [here](https://harp-tech.org/protocol/Device.html#table---list-of-available-common-registers). - - Attributes - ---------- - WHO_AM_I : int - The number of the `WHO_AM_I` register - HW_VERSION_H : int - The number of the `HW_VERSION_H` register - HW_VERSION_L : int - The number of the `HW_VERSION_L` register - ASSEMBLY_VERSION : int - The number of the `ASSEMBLY_VERSION` register - CORE_VERSION_H : int - The number of the `CORE_VERSION_H` register - CORE_VERSION_L : int - The number of the `CORE_VERSION_L` register - FIRMWARE_VERSION_H : int - The number of the `FIRMWARE_VERSION_H` register - FIRMWARE_VERSION_L : int - The number of the `FIRMWARE_VERSION_L` register - TIMESTAMP_SECOND : int - The number of the `TIMESTAMP_SECOND` register - TIMESTAMP_MICRO : int - The number of the `TIMESTAMP_MICRO` register - OPERATION_CTRL : int - The number of the `OPERATION_CTRL` register - RESET_DEV : int - The number of the `RESET_DEV` register - DEVICE_NAME : int - The number of the `DEVICE_NAME` register - SERIAL_NUMBER : int - The number of the `SERIAL_NUMBER` register - CLOCK_CONFIG : int - The number of the `CLOCK_CONFIG` register - TIMESTAMP_OFFSET : int - The number of the `TIMESTAMP_OFFSET` register - """ - - WHO_AM_I = 0x00 - HW_VERSION_H = 0x01 - HW_VERSION_L = 0x02 - ASSEMBLY_VERSION = 0x03 - CORE_VERSION_H = 0x04 - CORE_VERSION_L = 0x05 - FIRMWARE_VERSION_H = 0x06 - FIRMWARE_VERSION_L = 0x07 - TIMESTAMP_SECOND = 0x08 - TIMESTAMP_MICRO = 0x09 - OPERATION_CTRL = 0x0A - RESET_DEV = 0x0B - DEVICE_NAME = 0x0C - SERIAL_NUMBER = 0x0D - CLOCK_CONFIG = 0x0E - TIMESTAMP_OFFSET = 0x0F - - -class OperationMode(IntEnum): - """ - An enumeration with the operation modes of a Harp device. More information on the operation modes can be found [here](https://harp-tech.org/protocol/Device.html#r_operation_ctrl-u16--operation-mode-configuration). - - Attributes - ---------- - STANDBY : int - The value that corresponds to the Standby operation mode (0). The device has all the Events turned off - ACTIVE : int - The value that corresponds to the Active operation mode (1). The device turns ON the Events detection. Only the enabled Events will be operating - RESERVED : int - The value that corresponds to the Reserved operation mode (2) - SPEED : int - The value that corresponds to the Speed operation mode (3). The device enters Speed Mode - """ - - STANDBY = 0x00 - ACTIVE = 0x01 - RESERVED = 0x02 - SPEED = 0x03 - - -class OperationCtrl(IntFlag): - """ - An enumeration with the operation control bits of a Harp device. More information on the operation control bits can be found [here](https://harp-tech.org/protocol/Device.html#r_operation_ctrl-u16--operation-mode-configuration). - - Attributes - ---------- - OP_MODE : int - Operation mode of the device. - 0: Standby Mode (all Events off, mandatory) - 1: Active Mode (Events detection enabled, mandatory) - 2: Reserved - 3: Speed Mode (device enters Speed Mode, optional; only responds to Speed Mode commands) - DUMP : int - When set to 1, the device adds the content of all registers to the streaming buffer as Read messages. Always read as 0 - MUTE_RPL : int - If set to 1, replies to all commands are muted (not sent by the device) - VISUALEN : int - If set to 1, visual indications (e.g., LEDs) operate. If 0, all visual indications are turned off - OPLEDEN : int - If set to 1, the LED indicates the selected Operation Mode (see LED feedback table in documentation) - ALIVE_EN : int - If set to 1, the device sends an Event Message with the R_TIMESTAMP_SECONDS content each second (heartbeat) - """ - - OP_MODE = 0x03 - DUMP = 0x08 - MUTE_RPL = 0x10 - VISUALEN = 0x20 - OPLEDEN = 0x40 - ALIVE_EN = 0x80 - - -class ResetMode(IntEnum): - """ - An enumeration with the reset modes and actions for the R_RESET_DEV register of a Harp device. - More information on the reset modes can be found [here](https://harp-tech.org/protocol/Device.html#r_reset_dev-u8--reset-device-and-save-non-volatile-registers). - - Attributes - ---------- - RST_DEF : int - If set, resets the device and restores all registers (Common and Application) to default values. - EEPROM is erased and defaults become the permanent boot option - RST_EE : int - If set, resets the device and restores all registers (Common and Application) from non-volatile memory (EEPROM). - EEPROM values remain the permanent boot option - SAVE : int - If set, saves all non-volatile registers (Common and Application) to EEPROM and reboots. - EEPROM becomes the permanent boot option - NAME_TO_DEFAULT : int - If set, reboots the device with the default name - BOOT_DEF : int - If set, indicates the device booted with default register values - BOOT_EE : int - If set, indicates the device booted with register values saved on the EEPROM - """ - - RST_DEF = 0x01 - RST_EE = 0x02 - SAVE = 0x08 - NAME_TO_DEFAULT = 0x10 - BOOT_DEF = 0x40 - BOOT_EE = 0x80 - - -class ClockConfig(IntFlag): - """ - An enumeration with the clock configuration bits for the R_CLOCK_CONFIG register of a Harp device. - More information can be found [here](https://harp-tech.org/protocol/Device.html#r_clock_config-u8--synchronization-clock-configuration). - - Attributes - ---------- - CLK_REP : int - If set to 1, the device will repeat the Harp Synchronization Clock to the Clock Output connector, if available. - Acts as a daisy-chain by repeating the Clock Input to the Clock Output. Setting this bit also unlocks the Harp Synchronization Clock - CLK_GEN : int - If set to 1, the device will generate Harp Synchronization Clock to the Clock Output connector, if available. - The Clock Input will be ignored. Read as 1 if the device is generating the Harp Synchronization Clock - REP_ABLE : int - If set, indicates if the device is able (1) to repeat the Harp Synchronization Clock timestamp - GEN_ABLE : int - If set, indicates if the device is able (1) to generate the Harp Synchronization Clock timestamp - CLK_UNLOCK : int - If set to 1, the device will unlock the timestamp register counter (R_TIMESTAMP_SECOND) and accept new timestamp values. - Read as 1 if the timestamp register is unlocked - CLK_LOCK : int - If set to 1, the device will lock the current timestamp register counter (R_TIMESTAMP_SECOND) and reject new timestamp values. - Read as 1 if the timestamp register is locked - """ - - CLK_REP = 0x01 - CLK_GEN = 0x02 - REP_ABLE = 0x08 - GEN_ABLE = 0x10 - CLK_UNLOCK = 0x40 - CLK_LOCK = 0x80 diff --git a/src/harp-protocol/harp/protocol/exceptions.py b/src/harp-protocol/harp/protocol/exceptions.py deleted file mode 100644 index 0e9eb90..0000000 --- a/src/harp-protocol/harp/protocol/exceptions.py +++ /dev/null @@ -1,80 +0,0 @@ -from typing import Optional - -from harp.protocol.messages import HarpMessage - - -class HarpException(Exception): - """Base class for all exceptions raised related with Harp.""" - - def __init__(self, error_msg: str, message: Optional[HarpMessage] = None): - """ - Creates a new HarpException with the given error message. - - Parameters - ---------- - error_msg: str - The error message describing the exception. - message: Optional[HarpMessage] - The Harp message that caused the exception, if applicable. - """ - super().__init__(error_msg) - self.message = message - - -class HarpWriteException(HarpException): - """ - Exception raised when there is an error writing to a register in the Harp device. - """ - - def __init__(self, register_str: str, message: HarpMessage): - """ - Creates a new HarpWriteException for the given register. - - Parameters - ---------- - register_str: str - The register string where the write error occurred. - message: HarpMessage - The Harp message that caused the write error. - """ - super().__init__(f"Error writing to device on address {register_str}.", message) - - -class HarpReadException(HarpException): - """ - Exception raised when there is an error reading from a register in the Harp device. - """ - - def __init__(self, register_str: str, message: HarpMessage): - """ - Creates a new HarpReadException for the given register. - - Parameters - ---------- - register_str: str - The register string where the read error occurred. - message: HarpMessage - The Harp message that caused the read error. - """ - super().__init__(f'Error reading from register "{register_str}".', message) - - -class HarpTimeoutException(HarpException): - """Raised when no reply is received within the configured timeout.""" - - def __init__(self, timeout: float, message: HarpMessage): - """ - Creates a new HarpTimeoutException with the given timeout. - - Parameters - ---------- - timeout: float - The timeout duration in seconds. - message: HarpMessage - The Harp message that was sent when the timeout occurred. - """ - error_msg = ( - f"No reply received within {timeout} seconds for message:\r\n{message}" - ) - super().__init__(error_msg, message) - self.timeout = timeout diff --git a/src/harp-protocol/harp/protocol/messages.py b/src/harp-protocol/harp/protocol/messages.py deleted file mode 100644 index 58e9c0c..0000000 --- a/src/harp-protocol/harp/protocol/messages.py +++ /dev/null @@ -1,471 +0,0 @@ -from __future__ import annotations # for type hints (PEP 563) - -import struct -from typing import Optional, Union - -from harp.protocol import MessageType, PayloadType - - -class HarpMessage: - """ - The `HarpMessage` class implements the Harp message as described in the [protocol](https://harp-tech.org/protocol/BinaryProtocol-8bit.html). - - Attributes - ---------- - frame : bytearray - The bytearray containing the whole Harp message - message_type : MessageType - The message type - length : int - The length parameter of the Harp message - address : int - The address of the register to which the Harp message refers to - port : int - Indicates the origin or destination of the Harp message in case the device is a hub of Harp devices. The value 255 points to the device itself (default value). - payload_type : PayloadType - The payload type - checksum : int - The sum of all bytes contained in the Harp message - """ - - DEFAULT_PORT: int = 255 - BASE_LENGTH: int = 4 - _frame: bytearray = bytearray() - _port: int = DEFAULT_PORT - _timestamp: Optional[float] = None - _raw_payload: bytearray = bytearray() - - def __init__( - self, - message_type: MessageType, - payload_type: PayloadType, - address: int, - value: Optional[int | float | list[int] | list[float]] = None, - ): - """ - Parameters - ---------- - message_type : MessageType - The message type. - payload_type : PayloadType - The payload type. - address : int - The address of the register that the message will interact with. - value: int | list[int] | float | list[float], optional - The payload of the message. If message_type == MessageType.WRITE, the value cannot be None - """ - self._frame = bytearray() - payload = bytearray() - - if value is not None: - if isinstance(value, int) or isinstance(value, float): - values = [value] - else: - values = value - - for val in values: - if isinstance(val, float): - payload += struct.pack(" int: - """ - Calculates the checksum of the Harp message. - - Returns - ------- - int - The value of the checksum - """ - checksum: int = 0 - for i in self.frame: - checksum += i - return checksum & 255 - - @property - def frame(self) -> bytearray: - """ - The bytearray containing the whole Harp message. - - Returns - ------- - bytearray - The bytearray containing the whole Harp message - """ - return self._frame - - @property - def message_type(self) -> MessageType: - """ - The message type. - - Returns - ------- - MessageType - The message type - """ - return MessageType(self._frame[0]) - - @property - def length(self) -> int: - """ - The length parameter of the Harp message. - - Returns - ------- - int - The length parameter of the Harp message - """ - return self._frame[1] - - @property - def address(self) -> int: - """ - The address of the register to which the Harp message refers to. - - Returns - ------- - int - The address of the register to which the Harp message refers to - """ - return self._frame[2] - - @property - def port(self) -> int: - """ - Indicates the origin or destination of the Harp message in case the device is a hub of Harp devices. The value 255 points to the device itself (default value). - - Returns - ------- - int - The port value - """ - return self._frame[3] - - @port.setter - def port(self, value: int) -> None: - """ - Sets the port value. - - Parameters - ---------- - value : int - The port value to set - """ - self._port = value - - @property - def payload_type(self) -> PayloadType: - """ - The payload type. - - Returns - ------- - PayloadType - The payload type - """ - return PayloadType(self._frame[4]) - - @property - def timestamp(self) -> float | None: - return self._timestamp - - @property - def payload(self) -> Union[int, list[int], bytearray, float, list[float]]: - """ - The payload sent in the write Harp message. - - Returns - ------- - Union[int, list[int]] - The payload sent in the write Harp message - """ - payload_start = self.BASE_LENGTH - if self.payload_type.has_timestamp(): - payload_start += 6 - - payload_index = payload_start + 1 - - # length is payload_start + payload type size - pt = self.payload_type - if pt == PayloadType.U8 or pt == PayloadType.TIMESTAMPED_U8: - if self.length == payload_start + 1: - return self._frame[payload_index] - else: # array case - return [ - int.from_bytes([self._frame[i]], byteorder="little") - for i in range(payload_index, self.length + 1) - ] - - elif pt == PayloadType.S8 or pt == PayloadType.TIMESTAMPED_S8: - if self.length == payload_start + 1: - return int.from_bytes( - [self._frame[payload_index]], byteorder="little", signed=True - ) - else: # array case - return [ - int.from_bytes( - [self._frame[i]], - byteorder="little", - signed=True, - ) - for i in range(payload_index, self.length + 1) - ] - - elif pt == PayloadType.U16 or pt == PayloadType.TIMESTAMPED_U16: - if self.length == payload_start + 2: - return int.from_bytes( - self._frame[payload_index : payload_index + 2], - byteorder="little", - signed=False, - ) - else: # array case - return [ - int.from_bytes( - self._frame[i : i + 2], - byteorder="little", - signed=False, - ) - for i in range(payload_index, self.length + 1, 2) - ] - - elif pt == PayloadType.S16 or pt == PayloadType.TIMESTAMPED_S16: - if self.length == payload_start + 2: - return int.from_bytes( - self._frame[payload_index : payload_index + 2], - byteorder="little", - signed=True, - ) - else: - return [ - int.from_bytes( - self._frame[i : i + 2], - byteorder="little", - signed=True, - ) - for i in range(payload_index, self.length + 1, 2) - ] - - elif pt == PayloadType.U32 or pt == PayloadType.TIMESTAMPED_U32: - if self.length == payload_start + 4: - return int.from_bytes( - self._frame[payload_index : payload_index + 4], - byteorder="little", - signed=False, - ) - else: - return [ - int.from_bytes( - self._frame[i : i + 4], - byteorder="little", - signed=False, - ) - for i in range(payload_index, self.length + 1, 4) - ] - - elif pt == PayloadType.S32 or pt == PayloadType.TIMESTAMPED_S32: - if self.length == payload_start + 4: - return int.from_bytes( - self._frame[payload_index : payload_index + 4], - byteorder="little", - signed=True, - ) - else: - return [ - int.from_bytes( - self._frame[i : i + 4], - byteorder="little", - signed=True, - ) - for i in range(payload_index, self.length + 1, 4) - ] - - elif pt == PayloadType.U64 or pt == PayloadType.TIMESTAMPED_U64: - if self.length == payload_start + 8: - return int.from_bytes( - self._frame[payload_index : payload_index + 8], - byteorder="little", - signed=False, - ) - else: - return [ - int.from_bytes( - self._frame[i : i + 8], - byteorder="little", - signed=False, - ) - for i in range(payload_index, self.length + 1, 8) - ] - - elif pt == PayloadType.S64 or pt == PayloadType.TIMESTAMPED_S64: - if self.length == payload_start + 8: - return int.from_bytes( - self._frame[payload_index : payload_index + 8], - byteorder="little", - signed=True, - ) - else: - return [ - int.from_bytes( - self._frame[i : i + 8], - byteorder="little", - signed=True, - ) - for i in range(payload_index, self.length + 1, 8) - ] - - elif pt == PayloadType.FLOAT or pt == PayloadType.TIMESTAMPED_FLOAT: - if self.length == payload_start + 4: - return struct.unpack( - " int: - """ - The sum of all bytes contained in the Harp message. - - Returns - ------- - int - The sum of all bytes contained in the Harp message - """ - return self._frame[-1] - - @property - def is_error(self) -> bool: - """ - Indicates if this HarpMessage is an error message or not. - - Returns - ------- - bool - Returns True if this HarpMessage is an error message, False otherwise. - """ - return self.message_type.is_error() - - def payload_as_string(self) -> str: - """ - Returns the payload as a str. - - Returns - ------- - str - The payload parsed as a str - """ - return self._raw_payload.decode("utf-8").rstrip("\x00") - - @staticmethod - def parse(frame: bytearray) -> HarpMessage: - """ - Parses a bytearray to a (reply) Harp message. - - Parameters - ---------- - frame : bytearray - The bytearray will be parsed into a (reply) Harp message - - Returns - ------- - HarpMessage - The Harp message object parsed from the original bytearray - """ - message = HarpMessage(MessageType(frame[0]), PayloadType(frame[4]), frame[2]) - - message._frame = frame - - # assign timestamp if exists - if message.payload_type.has_timestamp(): - message._raw_payload = frame[11:-1] - message._timestamp = ( - int.from_bytes(frame[5:9], byteorder="little", signed=False) - + int.from_bytes(frame[9:11], byteorder="little", signed=False) * 32e-6 - ) - else: - message._raw_payload = frame[5:-1] - message._timestamp = None - - return message - - def __repr__(self) -> str: - """ - Prints debug representation of the reply message. - - Returns - ------- - str - The debug representation of the reply message - """ - return self.__str__() + f"\r\nRaw Frame: {self.frame}" - - def __str__(self) -> str: - """ - Prints friendly representation of a Harp message. - - Returns - ------- - str - The representation of the Harp message - """ - payload_str = "" - format_str = "" - if self.payload_type in [PayloadType.FLOAT, PayloadType.TIMESTAMPED_FLOAT]: - format_str = ".6f" - else: - bytes_per_word = self.payload_type & 0x07 - format_str = f"0{bytes_per_word}b" - - payload_str = "".join( - f"{item:{format_str}} " - for item in ( - self.payload if isinstance(self.payload, list) else [self.payload] - ) - ) - - # Check if the object has a 'timestamp' property and it's not None - timestamp_line = "" - if hasattr(self, "timestamp"): - ts = getattr(self, "timestamp") - if ts is not None: - timestamp_line = f"Timestamp: {ts}\r\n" - - return ( - f"Type: {self.message_type.name}\r\n" - + f"Length: {self.length}\r\n" - + f"Address: {self.address}\r\n" - + f"Port: {self.port}\r\n" - + timestamp_line - + f"Payload Type: {self.payload_type.name}\r\n" - + f"Payload Length: {len(self.payload) if self.payload is list else 1}\r\n" - + f"Payload: {payload_str}\r\n" - + f"Checksum: {self.checksum}" - ) diff --git a/src/harp-protocol/harp/protocol/py.typed b/src/harp-protocol/harp/protocol/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/uv.lock b/uv.lock index 1072820..d74a98a 100644 --- a/uv.lock +++ b/uv.lock @@ -1,15 +1,11 @@ version = 1 revision = 3 -requires-python = ">=3.10, <4.0" -resolution-markers = [ - "python_full_version >= '3.13'", - "python_full_version < '3.13'", -] +requires-python = ">=3.11, <4.0" [manifest] members = [ + "harp-device", "harp-protocol", - "harp-serial", "pyharp", ] @@ -60,17 +56,6 @@ version = "3.4.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695, upload-time = "2025-08-09T07:55:36.452Z" }, - { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153, upload-time = "2025-08-09T07:55:38.467Z" }, - { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428, upload-time = "2025-08-09T07:55:40.072Z" }, - { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627, upload-time = "2025-08-09T07:55:41.706Z" }, - { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388, upload-time = "2025-08-09T07:55:43.262Z" }, - { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077, upload-time = "2025-08-09T07:55:44.903Z" }, - { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631, upload-time = "2025-08-09T07:55:46.346Z" }, - { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210, upload-time = "2025-08-09T07:55:47.539Z" }, - { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739, upload-time = "2025-08-09T07:55:48.744Z" }, - { url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825, upload-time = "2025-08-09T07:55:50.305Z" }, - { url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452, upload-time = "2025-08-09T07:55:51.461Z" }, { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, @@ -145,16 +130,6 @@ version = "7.10.5" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/61/83/153f54356c7c200013a752ce1ed5448573dca546ce125801afca9e1ac1a4/coverage-7.10.5.tar.gz", hash = "sha256:f2e57716a78bc3ae80b2207be0709a3b2b63b9f2dcf9740ee6ac03588a2015b6", size = 821662, upload-time = "2025-08-23T14:42:44.78Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/70/e77b0061a6c7157bfce645c6b9a715a08d4c86b3360a7b3252818080b817/coverage-7.10.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c6a5c3414bfc7451b879141ce772c546985163cf553f08e0f135f0699a911801", size = 216774, upload-time = "2025-08-23T14:40:26.301Z" }, - { url = "https://files.pythonhosted.org/packages/91/08/2a79de5ecf37ee40f2d898012306f11c161548753391cec763f92647837b/coverage-7.10.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bc8e4d99ce82f1710cc3c125adc30fd1487d3cf6c2cd4994d78d68a47b16989a", size = 217175, upload-time = "2025-08-23T14:40:29.142Z" }, - { url = "https://files.pythonhosted.org/packages/64/57/0171d69a699690149a6ba6a4eb702814448c8d617cf62dbafa7ce6bfdf63/coverage-7.10.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:02252dc1216e512a9311f596b3169fad54abcb13827a8d76d5630c798a50a754", size = 243931, upload-time = "2025-08-23T14:40:30.735Z" }, - { url = "https://files.pythonhosted.org/packages/15/06/3a67662c55656702bd398a727a7f35df598eb11104fcb34f1ecbb070291a/coverage-7.10.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:73269df37883e02d460bee0cc16be90509faea1e3bd105d77360b512d5bb9c33", size = 245740, upload-time = "2025-08-23T14:40:32.302Z" }, - { url = "https://files.pythonhosted.org/packages/00/f4/f8763aabf4dc30ef0d0012522d312f0b7f9fede6246a1f27dbcc4a1e523c/coverage-7.10.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f8a81b0614642f91c9effd53eec284f965577591f51f547a1cbeb32035b4c2f", size = 247600, upload-time = "2025-08-23T14:40:33.66Z" }, - { url = "https://files.pythonhosted.org/packages/9c/31/6632219a9065e1b83f77eda116fed4c76fb64908a6a9feae41816dab8237/coverage-7.10.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6a29f8e0adb7f8c2b95fa2d4566a1d6e6722e0a637634c6563cb1ab844427dd9", size = 245640, upload-time = "2025-08-23T14:40:35.248Z" }, - { url = "https://files.pythonhosted.org/packages/6e/e2/3dba9b86037b81649b11d192bb1df11dde9a81013e434af3520222707bc8/coverage-7.10.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fcf6ab569436b4a647d4e91accba12509ad9f2554bc93d3aee23cc596e7f99c3", size = 243659, upload-time = "2025-08-23T14:40:36.815Z" }, - { url = "https://files.pythonhosted.org/packages/02/b9/57170bd9f3e333837fc24ecc88bc70fbc2eb7ccfd0876854b0c0407078c3/coverage-7.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:90dc3d6fb222b194a5de60af8d190bedeeddcbc7add317e4a3cd333ee6b7c879", size = 244537, upload-time = "2025-08-23T14:40:38.737Z" }, - { url = "https://files.pythonhosted.org/packages/b3/1c/93ac36ef1e8b06b8d5777393a3a40cb356f9f3dab980be40a6941e443588/coverage-7.10.5-cp310-cp310-win32.whl", hash = "sha256:414a568cd545f9dc75f0686a0049393de8098414b58ea071e03395505b73d7a8", size = 219285, upload-time = "2025-08-23T14:40:40.342Z" }, - { url = "https://files.pythonhosted.org/packages/30/95/23252277e6e5fe649d6cd3ed3f35d2307e5166de4e75e66aa7f432abc46d/coverage-7.10.5-cp310-cp310-win_amd64.whl", hash = "sha256:e551f9d03347196271935fd3c0c165f0e8c049220280c1120de0084d65e9c7ff", size = 220185, upload-time = "2025-08-23T14:40:42.026Z" }, { url = "https://files.pythonhosted.org/packages/cb/f2/336d34d2fc1291ca7c18eeb46f64985e6cef5a1a7ef6d9c23720c6527289/coverage-7.10.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c177e6ffe2ebc7c410785307758ee21258aa8e8092b44d09a2da767834f075f2", size = 216890, upload-time = "2025-08-23T14:40:43.627Z" }, { url = "https://files.pythonhosted.org/packages/39/ea/92448b07cc1cf2b429d0ce635f59cf0c626a5d8de21358f11e92174ff2a6/coverage-7.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:14d6071c51ad0f703d6440827eaa46386169b5fdced42631d5a5ac419616046f", size = 217287, upload-time = "2025-08-23T14:40:45.214Z" }, { url = "https://files.pythonhosted.org/packages/96/ba/ad5b36537c5179c808d0ecdf6e4aa7630b311b3c12747ad624dcd43a9b6b/coverage-7.10.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:61f78c7c3bc272a410c5ae3fde7792b4ffb4acc03d35a7df73ca8978826bb7ab", size = 247683, upload-time = "2025-08-23T14:40:46.791Z" }, @@ -229,18 +204,6 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] -[[package]] -name = "exceptiongroup" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, -] - [[package]] name = "fieldz" version = "0.1.2" @@ -315,14 +278,9 @@ wheels = [ ] [[package]] -name = "harp-protocol" -version = "0.4.0" -source = { editable = "src/harp-protocol" } - -[[package]] -name = "harp-serial" -version = "0.4.0" -source = { editable = "src/harp-serial" } +name = "harp-device" +version = "0.1.0" +source = { editable = "src/harp-device" } dependencies = [ { name = "harp-protocol" }, { name = "pyserial" }, @@ -334,6 +292,11 @@ requires-dist = [ { name = "pyserial", specifier = ">=3.5" }, ] +[[package]] +name = "harp-protocol" +version = "0.4.0" +source = { editable = "src/harp-protocol" } + [[package]] name = "idna" version = "3.10" @@ -379,16 +342,6 @@ version = "3.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" }, - { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" }, - { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" }, - { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" }, - { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" }, - { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" }, - { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" }, - { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" }, - { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" }, - { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" }, { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, @@ -614,7 +567,6 @@ dependencies = [ { name = "griffe" }, { name = "mkdocs-autorefs" }, { name = "mkdocstrings" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/13/d4/6327c4e82dda667b0ff83b6f6b6a03e7b81dfd1f28cd5eda50ffe66d546f/mkdocstrings_python-1.18.0.tar.gz", hash = "sha256:0b9924b4034fe9ae43604d78fe8e5107ea2c2391620124fc833043a62e83c744", size = 207601, upload-time = "2025-08-26T14:02:30.839Z" } wheels = [ @@ -680,15 +632,19 @@ name = "pyharp" version = "0.2.0" source = { virtual = "." } dependencies = [ + { name = "harp-device" }, { name = "harp-protocol" }, - { name = "harp-serial" }, ] [package.dev-dependencies] dev = [ + { name = "harp-device" }, + { name = "harp-protocol" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "ruff" }, + { name = "ty" }, + { name = "typing-extensions" }, ] docs = [ { name = "griffe-fieldz" }, @@ -704,15 +660,19 @@ docs = [ [package.metadata] requires-dist = [ + { name = "harp-device", editable = "src/harp-device" }, { name = "harp-protocol", editable = "src/harp-protocol" }, - { name = "harp-serial", editable = "src/harp-serial" }, ] [package.metadata.requires-dev] dev = [ + { name = "harp-device", editable = "src/harp-device" }, + { name = "harp-protocol", editable = "src/harp-protocol" }, { name = "pytest", specifier = ">=8.3.5" }, { name = "pytest-cov", specifier = ">=6.1.1" }, { name = "ruff", specifier = ">=0.11.0" }, + { name = "ty", specifier = ">=0.0.0" }, + { name = "typing-extensions", specifier = ">=4.15.0" }, ] docs = [ { name = "griffe-fieldz", specifier = ">=0.2.1" }, @@ -754,12 +714,10 @@ version = "8.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } wheels = [ @@ -810,15 +768,6 @@ version = "6.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, - { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, - { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, - { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, - { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, - { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, - { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, - { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, @@ -967,6 +916,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, ] +[[package]] +name = "ty" +version = "0.0.32" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/7e/2aa791c9ae7b8cd5024cd4122e92267f664ca954cea3def3211919fa3c1f/ty-0.0.32.tar.gz", hash = "sha256:8743174c5f920f6700a4a0c9de140109189192ba16226884cd50095b43b8a45c", size = 5522294, upload-time = "2026-04-20T19:29:01.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/eb/1075dc6a49d7acbe2584ae4d5b410c41b1f177a5adcc567e09eca4c69000/ty-0.0.32-py3-none-linux_armv6l.whl", hash = "sha256:dacbc2f6cd698d488ae7436838ff929570455bf94bfa4d9fe57a630c552aff83", size = 10902959, upload-time = "2026-04-20T19:28:31.907Z" }, + { url = "https://files.pythonhosted.org/packages/33/d2/c35fc8bc66e98d1ee9b0f8ed319bf743e450e1f1e997574b178fab75670f/ty-0.0.32-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914bbc4f605ce2a9e2a78982e28fae1d3359a169d141f9dc3b4c7749cd5eca81", size = 10726172, upload-time = "2026-04-20T19:28:44.765Z" }, + { url = "https://files.pythonhosted.org/packages/96/32/c827da3ca480456fb02d8cea68a2609273b6c220fea0be9a4c8d8470b86e/ty-0.0.32-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4787ac9fe1f86b1f3133f5c6732adbe2df5668b50c679ac6e2d98cd284da812f", size = 10163701, upload-time = "2026-04-20T19:28:27.005Z" }, + { url = "https://files.pythonhosted.org/packages/ba/9e/2734478fbdb90c160cb2813a3916a16a2af5c1e231f87d635f6131d781fb/ty-0.0.32-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ea0a728af99fe40dd744cba6441a2404f80b7f4bde17aa6da393810af5ea57", size = 10656220, upload-time = "2026-04-20T19:29:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/44/9f/0007da2d35e424debe7e9f86ffbc1ab7f60983cfbc5f0411324ab2de5292/ty-0.0.32-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2850561f9b018ae33d7e5bbfa0ac414d3c518513edcffe43877dc9801446b9c5", size = 10696086, upload-time = "2026-04-20T19:28:46.829Z" }, + { url = "https://files.pythonhosted.org/packages/3b/5e/ce5fd4ec803222ae3e69a76d2a2db2eed55e19f5b131702b9789ef45f93d/ty-0.0.32-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b5fa2fb3c614349ee211d36476b49d88c5ef79a687cdb91b2872ad023b94d2f8", size = 11184800, upload-time = "2026-04-20T19:28:42.57Z" }, + { url = "https://files.pythonhosted.org/packages/6c/46/ebcf67a5999421331214aac51a7464db42de2be15bbe929c612a3ed0b039/ty-0.0.32-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b89969307ab2417d41c9be8059dd79feea577234e1e10d35132f5495e0d42c6", size = 11718718, upload-time = "2026-04-20T19:28:36.433Z" }, + { url = "https://files.pythonhosted.org/packages/18/2c/2141c86ed0ce0962b45cefb658a95e734f59759d47f20afdcd9c732910a1/ty-0.0.32-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b59868ede9b1d69a088f0d695df52a0061f95fa7baa1d5e0dc6fc9cf06e1334", size = 11346369, upload-time = "2026-04-20T19:28:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/7a/da/ed6f772339cf29bd9a46def9d6db5084689eb574ee4d150ff704224c1ed8/ty-0.0.32-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8300caf35345498e9b9b03e550bba03cee8f5f5f8ab4c83c3b1ff1b7403b7d3a", size = 11280714, upload-time = "2026-04-20T19:28:51.516Z" }, + { url = "https://files.pythonhosted.org/packages/da/9b/c6813987edf4816a40e0c8e408b555f97d3f267c7b3a1688c8bbdf65609c/ty-0.0.32-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:583c7094f4574b02f724db924f98b804d1387a0bd9405ecb5e078cc0f47fbcfb", size = 10638806, upload-time = "2026-04-20T19:28:29.651Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d4/0cefcbd2ad0f3d51762ccf58e652ec7da146eb6ae34f87228f6254bbb8be/ty-0.0.32-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e44ebe1bb4143a5628bc4db67ac0dfebe14594af671e4ee66f6f2e983da56501", size = 10726106, upload-time = "2026-04-20T19:29:06.3Z" }, + { url = "https://files.pythonhosted.org/packages/32/ad/2c8a97f91f06311f4367400f7d13534bbda2522c73c99a3e4c0757dff9b8/ty-0.0.32-py3-none-musllinux_1_2_i686.whl", hash = "sha256:06f17ada3e069cba6148342ef88e9929156beca8473e8d4f101b68f66c75643e", size = 10872951, upload-time = "2026-04-20T19:28:34.077Z" }, + { url = "https://files.pythonhosted.org/packages/ba/68/42293f9248106dd51875120971a5cc6ea315c2c4dcfb8e59aa063aa0af26/ty-0.0.32-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e96e60fa556cec04f15d7ea62d2ceee5982bd389233e961ab9fd42304e278175", size = 11363334, upload-time = "2026-04-20T19:28:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/df/92/be9abf4d3e589ad5023e2ea965b93e204ec856420d46adf73c5c36c04678/ty-0.0.32-py3-none-win32.whl", hash = "sha256:2ff2ebb4986b24aebcf1444db7db5ca41b36086040e95eea9f8fb851c11e805c", size = 10260689, upload-time = "2026-04-20T19:28:56.541Z" }, + { url = "https://files.pythonhosted.org/packages/14/61/dc86acea899349d2579cb8419aecedd83dc504d7d6a10df65eef546c8300/ty-0.0.32-py3-none-win_amd64.whl", hash = "sha256:ba7284a4a954b598c1b31500352b3ec1f89bff533825592b5958848226fdc7ee", size = 11255371, upload-time = "2026-04-20T19:28:39.917Z" }, + { url = "https://files.pythonhosted.org/packages/43/01/beffec56d71ca25b343ede63adb076456b5b3e211f1c066452a44cd120b3/ty-0.0.32-py3-none-win_arm64.whl", hash = "sha256:7e10aadbdbda989a7d567ee6a37f8b98d4d542e31e3b190a2879fd581f75d658", size = 10658087, upload-time = "2026-04-20T19:28:59.286Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -991,9 +964,6 @@ version = "6.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, - { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, - { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, @@ -1003,8 +973,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, - { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, - { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, From 39f97ab78d5f92f1ed15636798c480b80724d25c Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sun, 26 Apr 2026 16:19:58 -0700 Subject: [PATCH 185/267] Add tests for protocol Co-authored-by: Copilot --- src/harp-protocol/pyproject.toml | 5 + tests/conftest.py | 8 + tests/fixtures.py | 22 ++ tests/protocol/__init__.py | 0 tests/protocol/test_checksum.py | 33 ++ tests/protocol/test_framer.py | 101 +++++ tests/protocol/test_message.py | 91 +++++ tests/protocol/test_message_type.py | 56 +++ tests/protocol/test_payload.py | 80 ++++ tests/protocol/test_payload_type.py | 63 ++++ tests/test_messages.py | 552 ---------------------------- uv.lock | 168 +++++++++ 12 files changed, 627 insertions(+), 552 deletions(-) create mode 100644 tests/conftest.py create mode 100644 tests/fixtures.py create mode 100644 tests/protocol/__init__.py create mode 100644 tests/protocol/test_checksum.py create mode 100644 tests/protocol/test_framer.py create mode 100644 tests/protocol/test_message.py create mode 100644 tests/protocol/test_message_type.py create mode 100644 tests/protocol/test_payload.py create mode 100644 tests/protocol/test_payload_type.py delete mode 100644 tests/test_messages.py diff --git a/src/harp-protocol/pyproject.toml b/src/harp-protocol/pyproject.toml index 937e974..b30c96c 100644 --- a/src/harp-protocol/pyproject.toml +++ b/src/harp-protocol/pyproject.toml @@ -6,6 +6,11 @@ authors = [{ name= "Hardware and Software Platform, Champalimaud Foundation", em license = "MIT" keywords = ['python', 'harp'] requires-python = ">=3.10,<4.0" +dependencies = [ + "numpy>=1.24", + "pandas>=2.0", + "typing-extensions>=4.0", +] [build-system] requires = ["uv_build>=0.9.5,<0.10.0"] diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..99c698b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,8 @@ +"""Pytest configuration — fixtures and path setup.""" + +from tests.fixtures import ( # re-export so conftest-aware code still works + TIMESTAMP_1S, + make_frame_from_raw, +) + +__all__ = ["make_frame_from_raw", "TIMESTAMP_1S"] diff --git a/tests/fixtures.py b/tests/fixtures.py new file mode 100644 index 0000000..d92f5f4 --- /dev/null +++ b/tests/fixtures.py @@ -0,0 +1,22 @@ + + +TIMESTAMP_1S: bytes = b"\x01\x00\x00\x00\x00\x00" + + +def make_frame_from_raw( + msg_type_byte: int, + address: int, + port: int, + payload_type: int, + payload: bytes, + *, + timestamp: bytes | None = None, +) -> bytes: + """Build a raw Harp frame from individual byte-level fields (for tests).""" + ts = timestamp if timestamp is not None else b"" + pt_byte = payload_type | (0x10 if ts else 0) + body = bytes([address, port, pt_byte]) + ts + payload + length = len(body) + 1 # +1 for checksum + frame = bytes([msg_type_byte, length]) + body + checksum = sum(frame) & 0xFF + return frame + bytes([checksum]) diff --git a/tests/protocol/__init__.py b/tests/protocol/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/protocol/test_checksum.py b/tests/protocol/test_checksum.py new file mode 100644 index 0000000..4230c1a --- /dev/null +++ b/tests/protocol/test_checksum.py @@ -0,0 +1,33 @@ +import pytest +from harp.protocol._checksum import compute, validate + + +@pytest.mark.parametrize( + "data, expected", + [ + ( + bytes([0x01, 0x04, 0x00, 0xFF, 0x01, 0x00]), + (0x01 + 0x04 + 0x00 + 0xFF + 0x01) & 0xFF, + ), + (bytes([0xFF] * 6), (0xFF * 5) & 0xFF), # wraps + (b"\xff", 0), # single byte: sums nothing + ], +) +def test_compute(data, expected): + assert compute(data) == expected + + +@pytest.mark.parametrize( + "frame, expected", + [ + (None, True), # None = auto-build a valid frame + (bytes([0x02, 0x05, 0x00, 0xFF, 0x01, 0xAB, 0x00]), False), # wrong checksum + (b"\xff", False), # too short + (b"", False), # empty + ], +) +def test_validate(frame, expected): + if frame is None: + body = bytes([0x02, 0x05, 0x00, 0xFF, 0x01]) + frame = body + bytes([sum(body) & 0xFF]) + assert validate(frame) == expected diff --git a/tests/protocol/test_framer.py b/tests/protocol/test_framer.py new file mode 100644 index 0000000..a4ff279 --- /dev/null +++ b/tests/protocol/test_framer.py @@ -0,0 +1,101 @@ +import struct + +from harp.protocol._framer import HarpFramer +from harp.protocol._message_type import MessageType + +from tests.fixtures import TIMESTAMP_1S, make_frame_from_raw + + +def test_single_message(): + frame = make_frame_from_raw(0x02, 10, 0xFF, 0x01, b"\x01") + msgs = HarpFramer.parse_bytes(frame) + assert len(msgs) == 1 + assert msgs[0].address == 10 + assert msgs[0].payload == b"\x01" + + +def test_back_to_back_messages(): + f1 = make_frame_from_raw(0x01, 8, 0xFF, 0x04, b"") + f2 = make_frame_from_raw(0x02, 10, 0xFF, 0x01, b"\x05") + f3 = make_frame_from_raw(0x03, 32, 0xFF, 0x11, b"\x7f", timestamp=TIMESTAMP_1S) + msgs = HarpFramer.parse_bytes(f1 + f2 + f3) + assert len(msgs) == 3 + assert msgs[0].message_type == MessageType.Read + assert msgs[1].message_type == MessageType.Write + assert msgs[2].message_type == MessageType.Event + + +def test_garbage_prefix_skipped(): + garbage = bytes([0x00, 0x05, 0xFF, 0x20, 0x00]) + frame = make_frame_from_raw(0x01, 8, 0xFF, 0x04, b"") + msgs = HarpFramer.parse_bytes(garbage + frame) + assert len(msgs) == 1 + assert msgs[0].address == 8 + + +def test_garbage_between_messages(): + f1 = make_frame_from_raw(0x01, 8, 0xFF, 0x04, b"") + f2 = make_frame_from_raw(0x02, 10, 0xFF, 0x01, b"\x05") + noise = bytes([0xAA, 0xBB, 0xCC]) + msgs = HarpFramer.parse_bytes(f1 + noise + f2) + assert len(msgs) == 2 + + +def test_bad_checksum_skipped_recovery(): + """A frame with a bad checksum should be skipped; the next valid frame parses.""" + bad = bytearray(make_frame_from_raw(0x01, 8, 0xFF, 0x04, b"")) + bad[-1] ^= 0xFF # corrupt checksum + good = make_frame_from_raw(0x02, 10, 0xFF, 0x01, b"\x05") + msgs = HarpFramer.parse_bytes(bytes(bad) + good) + assert len(msgs) == 1 + assert msgs[0].address == 10 + + +def test_truncated_stream_returns_empty(): + frame = make_frame_from_raw(0x01, 8, 0xFF, 0x04, b"") + # Feed only the first 3 bytes — not enough for a complete frame. + msgs = HarpFramer.parse_bytes(frame[:3]) + assert msgs == [] + + +def test_incremental_feed(): + """Feeding data in small chunks still yields the complete message.""" + frame = make_frame_from_raw(0x02, 10, 0xFF, 0x01, b"\x42") + framer = HarpFramer() + results = [] + for byte in frame: + framer.feed(bytes([byte])) + results.extend(framer.frames()) + assert len(results) == 1 + assert results[0].payload == b"\x42" + + +def test_all_scalar_types(): + """Framer correctly parses messages with each PayloadType.""" + from harp.protocol._payload_type import PayloadType, encode_payload_type + + for pt in PayloadType: + size = pt.numpy_dtype.itemsize + payload = bytes(range(size)) + pt_byte = encode_payload_type(pt) + frame = make_frame_from_raw(0x03, 32, 0xFF, pt_byte, payload) + msgs = HarpFramer.parse_bytes(frame) + assert len(msgs) == 1, f"Failed for {pt}" + assert msgs[0].payload_type == pt + + +def test_array_payload(): + payload = struct.pack("<" + "H" * 5, *range(5)) + frame = make_frame_from_raw(0x03, 32, 0xFF, 0x02, payload) + msgs = HarpFramer.parse_bytes(frame) + assert len(msgs) == 1 + assert len(msgs[0].payload) == 10 + + +def test_parse_file(tmp_path): + frame = make_frame_from_raw(0x01, 8, 0xFF, 0x04, b"") + p = tmp_path / "test.bin" + p.write_bytes(frame) + msgs = HarpFramer.parse_file(p) + assert len(msgs) == 1 + assert len(msgs) == 1 diff --git a/tests/protocol/test_message.py b/tests/protocol/test_message.py new file mode 100644 index 0000000..8e650af --- /dev/null +++ b/tests/protocol/test_message.py @@ -0,0 +1,91 @@ +import struct + +import numpy as np +import pytest +from harp.protocol._message import HarpParseError, parse +from harp.protocol._message_type import MessageType +from harp.protocol._payload_type import PayloadType + +from tests.fixtures import TIMESTAMP_1S, make_frame_from_raw + + +def test_parse_read_request(): + """Read request: no payload, no timestamp.""" + frame = make_frame_from_raw(0x01, address=8, port=0xFF, payload_type=0x04, payload=b"") + msg = parse(frame) + assert msg.message_type == MessageType.Read + assert msg.has_error is False + assert msg.address == 8 + assert msg.port == 0xFF + assert msg.payload == b"" + assert msg.timestamp is None + + +def test_parse_write_u8_payload(): + frame = make_frame_from_raw(0x02, address=10, port=0xFF, payload_type=0x01, payload=b"\x05") + msg = parse(frame) + assert msg.message_type == MessageType.Write + assert msg.payload == b"\x05" + assert msg.payload_type == PayloadType.U8 + + +def test_parse_with_timestamp(): + frame = make_frame_from_raw( + 0x03, + address=32, + port=0xFF, + payload_type=0x11, # U8 + HasTimestamp + payload=b"\x7f", + timestamp=TIMESTAMP_1S, + ) + msg = parse(frame) + assert msg.message_type == MessageType.Event + assert msg.timestamp == pytest.approx(1.0) + assert msg.payload == b"\x7f" + + +def test_parse_error_flag(): + frame = make_frame_from_raw(0x09, address=0, port=0xFF, payload_type=0x01, payload=b"\x00") + msg = parse(frame) + assert msg.has_error is True + assert msg.message_type == MessageType.Read + + +def test_parse_u16_array(): + payload = struct.pack(" NDArray[np.int16]: + return self._arr["x"] + + @property + def y(self) -> NDArray[np.uint8]: + return self._arr["y"] + + +class BitPackedPayload(PayloadBase): + _dtype: ClassVar = np.dtype([("packed", "u1")]) + + def to_dataframe(self) -> pd.DataFrame: + return pd.DataFrame( + { + "flag_a": (self._arr["packed"] & 0x01).astype(bool), + "flag_b": ((self._arr["packed"] >> 1) & 0x01).astype(bool), + } + ) + + +def _make_simple_bytes(n: int) -> bytes: + arr = np.zeros(n, dtype=SimplePayload._dtype) + arr["x"] = np.arange(n, dtype=np.int16) * -1 + arr["y"] = np.arange(n, dtype=np.uint8) + return arr.tobytes() + + +def test_from_buffer_shape(): + data = _make_simple_bytes(5) + p = SimplePayload.from_buffer(data) + assert len(p) == 5 + + +def test_from_buffer_values(): + data = _make_simple_bytes(3) + p = SimplePayload.from_buffer(data) + np.testing.assert_array_equal(p.x, [0, -1, -2]) + np.testing.assert_array_equal(p.y, [0, 1, 2]) + + +def test_to_dataframe_columns(): + p = SimplePayload.from_buffer(_make_simple_bytes(3)) + df = p.to_dataframe() + assert list(df.columns) == ["x", "y"] + assert len(df) == 3 + + +def test_to_dataframe_override(): + arr = np.array([(0b00000011,), (0b00000001,), (0b00000010,)], dtype=BitPackedPayload._dtype) + p = BitPackedPayload.from_buffer(arr.tobytes()) + df = p.to_dataframe() + assert list(df.columns) == ["flag_a", "flag_b"] + assert list(df["flag_a"]) == [True, True, False] + assert list(df["flag_b"]) == [True, False, True] + + +def test_from_buffer_zero_copy(): + data = _make_simple_bytes(4) + p = SimplePayload.from_buffer(data) + # np.frombuffer returns a read-only view — writes should raise + with pytest.raises((ValueError, TypeError)): + p._arr["x"][0] = 999 + + +def test_payload_property(): + p = SimplePayload.from_buffer(_make_simple_bytes(2)) + assert p.payload.dtype == SimplePayload._dtype diff --git a/tests/protocol/test_payload_type.py b/tests/protocol/test_payload_type.py new file mode 100644 index 0000000..963ed6e --- /dev/null +++ b/tests/protocol/test_payload_type.py @@ -0,0 +1,63 @@ +import pytest +from harp.protocol._payload_type import ( + PayloadType, + decode_payload_type, + encode_payload_type, +) + + +@pytest.mark.parametrize( + "byte, expected_type, has_ts", + [ + (0x01, PayloadType.U8, False), + (0x02, PayloadType.U16, False), + (0x04, PayloadType.U32, False), + (0x08, PayloadType.U64, False), + (0x81, PayloadType.S8, False), + (0x82, PayloadType.S16, False), + (0x84, PayloadType.S32, False), + (0x88, PayloadType.S64, False), + (0x44, PayloadType.Float, False), + # With timestamp bit + (0x11, PayloadType.U8, True), + (0x12, PayloadType.U16, True), + (0x14, PayloadType.U32, True), + (0x18, PayloadType.U64, True), + (0x91, PayloadType.S8, True), + (0x92, PayloadType.S16, True), + (0x94, PayloadType.S32, True), + (0x98, PayloadType.S64, True), + (0x54, PayloadType.Float, True), + ], +) +def test_decode_valid(byte, expected_type, has_ts): + info = decode_payload_type(byte) + assert info.payload_type == expected_type + assert info.has_timestamp == has_ts + assert info.element_size == expected_type.numpy_dtype.itemsize + + +@pytest.mark.parametrize( + "byte", + [ + 0x00, # size nibble = 0 + 0x03, # size nibble = 3 + 0x05, # size nibble = 5 + 0x20, # reserved bit 5 set + 0xC4, # IsFloat + IsSigned + 0x41, # IsFloat + size=1 (8-bit float invalid) + 0x42, # IsFloat + size=2 + ], +) +def test_decode_invalid(byte): + with pytest.raises(ValueError): + decode_payload_type(byte) + + +def test_encode_roundtrip(): + for pt in PayloadType: + for has_ts in (False, True): + b = encode_payload_type(pt, has_timestamp=has_ts) + info = decode_payload_type(b) + assert info.payload_type == pt + assert info.has_timestamp == has_ts diff --git a/tests/test_messages.py b/tests/test_messages.py deleted file mode 100644 index c9a167c..0000000 --- a/tests/test_messages.py +++ /dev/null @@ -1,552 +0,0 @@ -import pytest - -from harp.protocol import CommonRegisters, MessageType, PayloadType -from harp.protocol.exceptions import HarpException, HarpReadException -from harp.protocol.messages import HarpMessage - -DEFAULT_ADDRESS = 42 - - -def test_create_write_float(): - """Test creating a write message with float value.""" - value = 3.14159 - message = HarpMessage(MessageType.WRITE, PayloadType.FLOAT, 42, value) - - assert message.message_type == MessageType.WRITE - assert abs(message.payload - value) < 0.0001 # Float comparison - assert len(message.frame) == 10 # 5 header bytes + 4 float bytes + 1 checksum - - -def test_create_write_list(): - """Test creating a write message with list values.""" - values = [10, 20, 30] - message = HarpMessage(MessageType.WRITE, PayloadType.U8, 42, values) - - # The frame should have length: 1 (type) + 1 (length) + 1 (address) + 1 (port) + 1 (payload_type) + 3 (values) + 1 (checksum) - assert len(message.frame) == 9 - - # Extract the payload portion from the frame - payload_bytes = message.frame[5:8] - # Verify each value in the payload bytes - assert list(payload_bytes) == values - - -def test_reply_is_error(): - """Test HarpMessage.is_error property.""" - # Create a READ_ERROR message - frame = bytearray( - [ - MessageType.READ_ERROR, - 5, - 42, - 255, - PayloadType.TIMESTAMPED_U8, - 0, - 0, - 0, - 0, # timestamp seconds - 0, - 0, # timestamp micros - 123, # payload - 0, - ] - ) # checksum placeholder - - # Fix checksum - checksum = sum(frame[:-1]) & 255 - frame[-1] = checksum - - reply = HarpMessage.parse(frame) - assert reply.is_error - - # Create a normal READ message - frame = bytearray( - [ - MessageType.READ, - 5, - 42, - 255, - PayloadType.TIMESTAMPED_U8, - 0, - 0, - 0, - 0, # timestamp seconds - 0, - 0, # timestamp micros - 123, # payload - 0, - ] - ) # checksum placeholder - - # Fix checksum - checksum = sum(frame[:-1]) & 255 - frame[-1] = checksum - - reply = HarpMessage.parse(frame) - assert not reply.is_error - - -def test_create_read_U8() -> None: - message = HarpMessage(MessageType.READ, PayloadType.U8, DEFAULT_ADDRESS) - - assert message.message_type == MessageType.READ - assert message.checksum == 47 # 1 + 4 + 42 + 255 + 1 - 256 - - -def test_create_read_S8() -> None: - message = HarpMessage(MessageType.READ, PayloadType.S8, DEFAULT_ADDRESS) - - assert message.message_type == MessageType.READ - assert message.checksum == 175 # 1 + 4 + 42 + 255 + 129 - 256 - - -def test_create_read_U16() -> None: - message = HarpMessage(MessageType.READ, PayloadType.U16, DEFAULT_ADDRESS) - - assert message.message_type == MessageType.READ - assert message.checksum == 48 # 1 + 4 + 42 + 255 + 2 - 256 - - -def test_create_read_S16() -> None: - message = HarpMessage(MessageType.READ, PayloadType.S16, DEFAULT_ADDRESS) - - assert message.message_type == MessageType.READ - assert message.checksum == 176 # 1 + 4 + 42 + 255 + 130 - 256 - - -def test_create_read_U32() -> None: - message = HarpMessage(MessageType.READ, PayloadType.U32, DEFAULT_ADDRESS) - - assert message.message_type == MessageType.READ - assert message.checksum == 50 # 1 + 4 + 42 + 255 + 4 - 256 - - -def test_create_read_S32() -> None: - message = HarpMessage(MessageType.READ, PayloadType.S32, DEFAULT_ADDRESS) - - assert message.message_type == MessageType.READ - assert message.checksum == 178 # 1 + 4 + 42 + 255 + 130 - 256 - - -def test_create_read_U64() -> None: - message = HarpMessage(MessageType.READ, PayloadType.U64, DEFAULT_ADDRESS) - - assert message.message_type == MessageType.READ - assert message.checksum == 54 # 1 + 4 + 42 + 255 + 2 - 256 - - -def test_create_read_S64() -> None: - message = HarpMessage(MessageType.READ, PayloadType.S64, DEFAULT_ADDRESS) - - assert message.message_type == MessageType.READ - assert message.checksum == 182 # 1 + 4 + 42 + 255 + 130 - 256 - - -def test_create_read_float() -> None: - message = HarpMessage(MessageType.READ, PayloadType.FLOAT, DEFAULT_ADDRESS) - - assert message.message_type == MessageType.READ - assert message.checksum == 114 # 1 + 4 + 42 + 255 + 4 - 256 - - -def test_create_write_U8() -> None: - value: int = 23 - message = HarpMessage(MessageType.WRITE, PayloadType.U8, DEFAULT_ADDRESS, value) - - assert message.message_type == MessageType.WRITE - assert message.payload == value - assert message.checksum == 72 # 2 + 4 + 42 + 255 + 1 + 23 = 328 → 328 % 256 = 73 - - -def test_create_write_S8() -> None: - value: int = -3 # corresponds to signed int 253 (0xFD) - message = HarpMessage(MessageType.WRITE, PayloadType.S8, DEFAULT_ADDRESS, value) - - assert message.message_type == MessageType.WRITE - assert message.payload == value - assert message.checksum == 174 # (2 + 5 + 42 + 255 + 129 + 253) & 255 - - -def test_create_write_U16() -> None: - value: int = 1024 # 4 0 (2 x bytes) - message = HarpMessage(MessageType.WRITE, PayloadType.U16, DEFAULT_ADDRESS, value) - - assert message.message_type == MessageType.WRITE - assert message.length == 6 - assert message.payload == value - assert message.checksum == 55 # (2 + 6 + 42 + 255 + 2 + 4 + 0) & 255 - - -def test_create_write_S16() -> None: - value: int = -4837 # 27 237 (2 x bytes), corresponds to signed int 7149 - message = HarpMessage(MessageType.WRITE, PayloadType.S16, DEFAULT_ADDRESS, value) - - assert message.message_type == MessageType.WRITE - assert message.length == 6 - assert message.payload == value - assert message.checksum == 187 # (2 + 6 + 42 + 255 + 130 + 27 + 237) & 255 - - -def test_create_write_U8_array() -> None: - values: list[int] = [1, 2, 3, 4, 5] - message = HarpMessage(MessageType.WRITE, PayloadType.U8, DEFAULT_ADDRESS, values) - - assert message.message_type == MessageType.WRITE - assert message.length == 4 + len( - values - ) # 7 header bytes + len(values) payload bytes - assert message.payload == values - assert message.checksum == 68 # (2 + (4 + 5) + 42 + 255 + 1 + 5) & 255 - - -def test_create_write_S8_array() -> None: - values: list[int] = [-1, -2, -3, -4, -5] - message = HarpMessage(MessageType.WRITE, PayloadType.S8, DEFAULT_ADDRESS, values) - - assert message.message_type == MessageType.WRITE - assert message.length == 4 + len( - values - ) # 7 header bytes + len(values) payload bytes - assert message.payload == values - assert message.checksum == 166 # (2 + (4 + 5) + 42 + 255 + 129 + 5) & 255 - - -def test_create_write_U16_array() -> None: - values: list[int] = [1, 2, 3, 4, 5] - message = HarpMessage(MessageType.WRITE, PayloadType.U16, DEFAULT_ADDRESS, values) - - assert message.message_type == MessageType.WRITE - assert ( - message.length == 4 + len(values) * 2 - ) # 7 header bytes + len(values) * 2 payload bytes - assert message.payload == values - assert message.checksum == 74 # (2 + (4 + 5 * 2) + 42 + 255 + 2 + 5) & 255 - - -def test_create_write_S16_array() -> None: - values: list[int] = [-1, -2, -3, -4, -5] - message = HarpMessage(MessageType.WRITE, PayloadType.S16, DEFAULT_ADDRESS, values) - - assert message.message_type == MessageType.WRITE - assert ( - message.length == 4 + len(values) * 2 - ) # 7 header bytes + len(values) * 2 payload bytes - assert message.payload == values - assert message.checksum == 167 # (2 + (4 + 5) + 42 + 255 + 129 + 5) & 255 - - -def test_create_write_U32_array() -> None: - values: list[int] = [1, 2, 3, 4, 5] - message = HarpMessage(MessageType.WRITE, PayloadType.U32, DEFAULT_ADDRESS, values) - - assert message.message_type == MessageType.WRITE - assert ( - message.length == 4 + len(values) * 4 - ) # 7 header bytes + len(values) * 4 payload bytes - assert message.payload == values - assert message.checksum == 86 # (2 + (4 + 5 * 4) + 42 + 255 + 4 + 5) & 255 - - -def test_create_write_S32_array() -> None: - values: list[int] = [-1, -2, -3, -4, -5] - message = HarpMessage(MessageType.WRITE, PayloadType.S32, DEFAULT_ADDRESS, values) - - assert message.message_type == MessageType.WRITE - assert ( - message.length == 4 + len(values) * 4 - ) # 7 header bytes + len(values) * 4 payload bytes - assert message.payload == values - assert message.checksum == 169 # (2 + (4 + 5 * 4) + 42 + 255 + 130 + 5) & 255 - - -def test_create_write_U64_array() -> None: - values: list[int] = [1, 2, 3, 4, 5] - message = HarpMessage(MessageType.WRITE, PayloadType.U64, DEFAULT_ADDRESS, values) - - assert message.message_type == MessageType.WRITE - assert ( - message.length == 4 + len(values) * 8 - ) # 7 header bytes + len(values) * 8 payload bytes - assert message.payload == values - assert message.checksum == 110 # (2 + (4 + 5 * 8) + 42 + 255 + 2 + 5) & 255 - - -def test_create_write_S64_array() -> None: - values: list[int] = [-1, -2, -3, -4, -5] - message = HarpMessage(MessageType.WRITE, PayloadType.S64, DEFAULT_ADDRESS, values) - - assert message.message_type == MessageType.WRITE - assert ( - message.length == 4 + len(values) * 8 - ) # 7 header bytes + len(values) * 8 payload bytes - assert message.payload == values - assert message.checksum == 173 # (2 + (4 + 5 * 8) + 42 + 255 + 130 + 5) & 255 - - -def test_create_write_float_array() -> None: - """Test creating a write message with float array values.""" - values = [1.1, 2.2, 3.3] - message = HarpMessage(MessageType.WRITE, PayloadType.FLOAT, DEFAULT_ADDRESS, values) - - assert message.message_type == MessageType.WRITE - expected_checksum = 193 # (2 + 4 + 42 + 255 + 1 + 3 * 4) & 255 - assert len(message.payload) == len(values) - for actual, expected in zip(message.payload, values): - assert abs(actual - expected) < 0.0001 # Float comparison with error margin - assert message.checksum == expected_checksum - - -def test_read_who_am_i() -> None: - message = HarpMessage(MessageType.READ, PayloadType.U16, CommonRegisters.WHO_AM_I) - - assert str(message.frame) == str(bytearray(b"\x01\x04\x00\xff\x02\x06")) - - -def test_create_write_U32() -> None: - """Test creating a write message with S32 value.""" - value: int = 2147483000 # Large number - message = HarpMessage(MessageType.WRITE, PayloadType.U32, DEFAULT_ADDRESS, value) - - assert message.message_type == MessageType.WRITE - assert message.length == 8 - assert message.payload == value - assert len(message.frame) == 10 # length + checksum byte - # Calculate checksum as in other tests - expected_checksum = 42 # (2 + 8 + 42 + 255 + 4 + 0 + 0 + 0 + 0) & 255 - assert message.checksum == expected_checksum - - -def test_create_write_S32() -> None: - """Test creating a write message with S32 value.""" - value: int = -2147483000 # Large negative number - message = HarpMessage(MessageType.WRITE, PayloadType.S32, DEFAULT_ADDRESS, value) - - assert message.message_type == MessageType.WRITE - assert message.length == 8 - assert message.payload == value - assert len(message.frame) == 10 # length + checksum byte - # Calculate checksum as in other tests - expected_checksum = 193 # (2 + 8 + 42 + 255 + 130 + 0 + 0 + 0 + 0) & 255 - assert message.checksum == expected_checksum - - -def test_create_write_U64() -> None: - """Test creating a write message with U64 value.""" - value: int = 9223372036854775807 # Large 64-bit value - message = HarpMessage(MessageType.WRITE, PayloadType.U64, DEFAULT_ADDRESS, value) - - assert message.message_type == MessageType.WRITE - assert message.length == 12 # 5 header bytes + 8 payload bytes - assert message.payload == value - assert len(message.frame) == 14 # length + checksum byte - # Calculate checksum for 64-bit value - expected_checksum = 183 # (2 + 12 + 42 + 255 + 2 + 0 + 0 + 0 + 0) & 255 - assert message.checksum == expected_checksum - - -def test_create_write_S64() -> None: - """Test creating a write message with S64 value.""" - value: int = -9223372036854775807 - message = HarpMessage(MessageType.WRITE, PayloadType.S64, DEFAULT_ADDRESS, value) - assert message.message_type == MessageType.WRITE - assert message.length == 12 - assert message.payload == value - assert len(message.frame) == 14 # length + checksum byte - # Calculate checksum for 64-bit signed value - expected_checksum = 64 # (2 + 12 + 42 + 255 + 130 + 0 + 0 + 0 + 0) & 255 - assert message.checksum == expected_checksum - - -def test_reply_message_str_repr() -> None: - """Test string representation of Reply message.""" - # Create a simple reply message - frame = bytearray( - [ - MessageType.READ, - 5, - 42, - 255, - PayloadType.TIMESTAMPED_U8, - 0, - 0, - 0, - 0, # timestamp seconds - 0, - 0, # timestamp micros - 123, # payload - 0, - ] - ) # checksum placeholder - - # Fix checksum - checksum = sum(frame[:-1]) & 255 - frame[-1] = checksum - - reply = HarpMessage.parse(frame) - str_repr = str(reply) - repr_str = repr(reply) - - assert "Type: READ" in str_repr - assert "Length: 5" in str_repr - assert "Address: 42" in str_repr - assert "Port: 255" in str_repr - assert "Payload: " in str_repr - assert "Raw Frame" in repr_str - - -def test_payload_as_string() -> None: - """Test HarpMessage.payload_as_string().""" - test_string = "Hello" - encoded = test_string.encode("utf-8") - - frame = bytearray( - [ - MessageType.READ, - 5 + len(encoded), - 42, - 255, - PayloadType.TIMESTAMPED_U8, - 0, - 0, - 0, - 0, # timestamp seconds - 0, - 0, - ] - ) # timestamp micros - - # Add string payload - frame.extend(encoded) - # Add checksum - frame.append(0) # placeholder - - # Fix checksum - checksum = sum(frame[:-1]) & 255 - frame[-1] = checksum - - reply = HarpMessage.parse(frame) - assert reply.payload_as_string() == test_string - - -def test_harp_message_parse() -> None: - """Test the static parse method of HarpMessage.""" - frame = bytearray( - [ - MessageType.READ, - 11, - 42, - 255, - PayloadType.TIMESTAMPED_U8, - 0, - 0, - 0, - 0, # timestamp seconds - 0, - 0, # timestamp micros - 123, # payload - 0, - ] - ) # checksum placeholder - - # Fix checksum - checksum = sum(frame[:-1]) & 255 - frame[-1] = checksum - - message = HarpMessage.parse(frame) - assert message.message_type == MessageType.READ - assert message.address == 42 - assert message.payload == 123 - - -def test_timestamp_handling() -> None: - """Test timestamp handling in HarpMessage.""" - # Create a timestamped message - frame = bytearray( - [ - MessageType.READ, - 5, - 42, - 255, - PayloadType.TIMESTAMPED_U8, - 1, - 0, - 0, - 0, # timestamp seconds = 1 - 32, - 0, # timestamp micros = 32 (= 1ms) - 123, # payload - 0, - ] - ) # checksum placeholder - - # Fix checksum - checksum = sum(frame[:-1]) & 255 - frame[-1] = checksum - - reply = HarpMessage.parse(frame) - assert reply.timestamp is not None - assert reply.timestamp == 1 + 32 * 32e-6 # 1 second + 1 millisecond - - -# FIXME: handle this better -def test_calculate_checksum() -> None: - """Test the calculate_checksum method.""" - message = HarpMessage(MessageType.READ, PayloadType.U8, DEFAULT_ADDRESS) - message._frame = bytearray([1, 2, 3, 4, 5]) - - # Sum is 15, checksum is 15 (no overflow) - assert message.calculate_checksum() == 15 - - message._frame = bytearray([200, 100, 50, 20, 10]) - # Sum is 380, checksum is 380 % 256 = 124 - assert message.calculate_checksum() == 124 - - -# create harpMessage test, check _raw_payload and change frame and recheck raw_payload -def test_raw_payload_assignment() -> None: - """Test that _raw_payload is assigned correctly based on payload type.""" - # Create a timestamped message frame - frame = bytearray( - [ - MessageType.READ, - 11, - 42, - 255, - PayloadType.TIMESTAMPED_U8, - 0, - 0, - 0, - 0, # timestamp seconds - 0, - 0, # timestamp micros - 123, # payload - 0, - ] - ) # checksum placeholder - - # Fix checksum - checksum = sum(frame[:-1]) & 255 - frame[-1] = checksum - - message = HarpMessage.parse(frame) - assert message._raw_payload == frame[11:-1] - - # Create a non-timestamped message frame - frame_no_timestamp = bytearray( - [ - MessageType.READ, - 5, - 42, - 255, - PayloadType.U8, - 123, # payload - 0, - ] - ) # checksum placeholder - - # Fix checksum - checksum = sum(frame_no_timestamp[:-1]) & 255 - frame_no_timestamp[-1] = checksum - - message_no_timestamp = HarpMessage.parse(frame_no_timestamp) - assert message_no_timestamp._raw_payload == frame_no_timestamp[5:-1] diff --git a/uv.lock b/uv.lock index d74a98a..be04bd4 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,14 @@ version = 1 revision = 3 requires-python = ">=3.11, <4.0" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] [manifest] members = [ @@ -296,6 +304,18 @@ requires-dist = [ name = "harp-protocol" version = "0.4.0" source = { editable = "src/harp-protocol" } +dependencies = [ + { name = "numpy" }, + { name = "pandas" }, + { name = "typing-extensions" }, +] + +[package.metadata] +requires-dist = [ + { name = "numpy", specifier = ">=1.24" }, + { name = "pandas", specifier = ">=2.0" }, + { name = "typing-extensions", specifier = ">=4.0" }, +] [[package]] name = "idna" @@ -573,6 +593,85 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/96/7ecc71bb9f01ee20f201b2531960b401159c6730aec90ec76a1b74bc81e1/mkdocstrings_python-1.18.0-py3-none-any.whl", hash = "sha256:f5056d8afe9a9683ad0c59001df1ecd9668b51c19b9a6b4dc0ff02cc9b76265a", size = 138182, upload-time = "2025-08-26T14:02:28.076Z" }, ] +[[package]] +name = "numpy" +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/c6/4218570d8c8ecc9704b5157a3348e486e84ef4be0ed3e38218ab473c83d2/numpy-2.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db", size = 16976799, upload-time = "2026-03-29T13:18:15.438Z" }, + { url = "https://files.pythonhosted.org/packages/dd/92/b4d922c4a5f5dab9ed44e6153908a5c665b71acf183a83b93b690996e39b/numpy-2.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0", size = 14971552, upload-time = "2026-03-29T13:18:18.606Z" }, + { url = "https://files.pythonhosted.org/packages/8a/dc/df98c095978fa6ee7b9a9387d1d58cbb3d232d0e69ad169a4ce784bde4fd/numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015", size = 5476566, upload-time = "2026-03-29T13:18:21.532Z" }, + { url = "https://files.pythonhosted.org/packages/28/34/b3fdcec6e725409223dd27356bdf5a3c2cc2282e428218ecc9cb7acc9763/numpy-2.4.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40", size = 6806482, upload-time = "2026-03-29T13:18:23.634Z" }, + { url = "https://files.pythonhosted.org/packages/68/62/63417c13aa35d57bee1337c67446761dc25ea6543130cf868eace6e8157b/numpy-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d", size = 15973376, upload-time = "2026-03-29T13:18:26.677Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c5/9fcb7e0e69cef59cf10c746b84f7d58b08bc66a6b7d459783c5a4f6101a6/numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502", size = 16925137, upload-time = "2026-03-29T13:18:30.14Z" }, + { url = "https://files.pythonhosted.org/packages/7e/43/80020edacb3f84b9efdd1591120a4296462c23fd8db0dde1666f6ef66f13/numpy-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd", size = 17329414, upload-time = "2026-03-29T13:18:33.733Z" }, + { url = "https://files.pythonhosted.org/packages/fd/06/af0658593b18a5f73532d377188b964f239eb0894e664a6c12f484472f97/numpy-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5", size = 18658397, upload-time = "2026-03-29T13:18:37.511Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ce/13a09ed65f5d0ce5c7dd0669250374c6e379910f97af2c08c57b0608eee4/numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e", size = 6239499, upload-time = "2026-03-29T13:18:40.372Z" }, + { url = "https://files.pythonhosted.org/packages/bd/63/05d193dbb4b5eec1eca73822d80da98b511f8328ad4ae3ca4caf0f4db91d/numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e", size = 12614257, upload-time = "2026-03-29T13:18:42.95Z" }, + { url = "https://files.pythonhosted.org/packages/87/c5/8168052f080c26fa984c413305012be54741c9d0d74abd7fbeeccae3889f/numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e", size = 10486775, upload-time = "2026-03-29T13:18:45.835Z" }, + { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, + { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, + { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, + { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" }, + { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, + { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, + { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, + { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, + { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" }, + { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, + { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, + { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, + { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, + { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, + { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, + { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, + { url = "https://files.pythonhosted.org/packages/6b/33/8fae8f964a4f63ed528264ddf25d2b683d0b663e3cba26961eb838a7c1bd/numpy-2.4.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4", size = 16854491, upload-time = "2026-03-29T13:21:38.03Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d0/1aabee441380b981cf8cdda3ae7a46aa827d1b5a8cce84d14598bc94d6d9/numpy-2.4.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e", size = 14895830, upload-time = "2026-03-29T13:21:41.509Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b8/aafb0d1065416894fccf4df6b49ef22b8db045187949545bced89c034b8e/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c", size = 5400927, upload-time = "2026-03-29T13:21:44.747Z" }, + { url = "https://files.pythonhosted.org/packages/d6/77/063baa20b08b431038c7f9ff5435540c7b7265c78cf56012a483019ca72d/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3", size = 6715557, upload-time = "2026-03-29T13:21:47.406Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a8/379542d45a14f149444c5c4c4e7714707239ce9cc1de8c2803958889da14/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7", size = 15804253, upload-time = "2026-03-29T13:21:50.753Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c8/f0a45426d6d21e7ea3310a15cf90c43a14d9232c31a837702dba437f3373/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f", size = 16753552, upload-time = "2026-03-29T13:21:54.344Z" }, + { url = "https://files.pythonhosted.org/packages/04/74/f4c001f4714c3ad9ce037e18cf2b9c64871a84951eaa0baf683a9ca9301c/numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119", size = 12509075, upload-time = "2026-03-29T13:21:57.644Z" }, +] + [[package]] name = "packaging" version = "25.0" @@ -591,6 +690,66 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, ] +[[package]] +name = "pandas" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/99/b342345300f13440fe9fe385c3c481e2d9a595ee3bab4d3219247ac94e9a/pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043", size = 4645855, upload-time = "2026-03-31T06:48:30.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/35/6411db530c618e0e0005187e35aa02ce60ae4c4c4d206964a2f978217c27/pandas-3.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a727a73cbdba2f7458dc82449e2315899d5140b449015d822f515749a46cbbe0", size = 10326926, upload-time = "2026-03-31T06:46:08.29Z" }, + { url = "https://files.pythonhosted.org/packages/c4/d3/b7da1d5d7dbdc5ef52ed7debd2b484313b832982266905315dad5a0bf0b1/pandas-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dbbd4aa20ca51e63b53bbde6a0fa4254b1aaabb74d2f542df7a7959feb1d760c", size = 9926987, upload-time = "2026-03-31T06:46:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/52/77/9b1c2d6070b5dbe239a7bc889e21bfa58720793fb902d1e070695d87c6d0/pandas-3.0.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:339dda302bd8369dedeae979cb750e484d549b563c3f54f3922cb8ff4978c5eb", size = 10757067, upload-time = "2026-03-31T06:46:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/20/17/ec40d981705654853726e7ac9aea9ddbb4a5d9cf54d8472222f4f3de06c2/pandas-3.0.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61c2fd96d72b983a9891b2598f286befd4ad262161a609c92dc1652544b46b76", size = 11258787, upload-time = "2026-03-31T06:46:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/90/e3/3f1126d43d3702ca8773871a81c9f15122a1f412342cc56284ffda5b1f70/pandas-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c934008c733b8bbea273ea308b73b3156f0181e5b72960790b09c18a2794fe1e", size = 11771616, upload-time = "2026-03-31T06:46:20.532Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cf/0f4e268e1f5062e44a6bda9f925806721cd4c95c2b808a4c82ebe914f96b/pandas-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:60a80bb4feacbef5e1447a3f82c33209c8b7e07f28d805cfd1fb951e5cb443aa", size = 12337623, upload-time = "2026-03-31T06:46:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/97a6339859d4acb2536efb24feb6708e82f7d33b2ed7e036f2983fcced82/pandas-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:ed72cb3f45190874eb579c64fa92d9df74e98fd63e2be7f62bce5ace0ade61df", size = 9897372, upload-time = "2026-03-31T06:46:26.703Z" }, + { url = "https://files.pythonhosted.org/packages/8f/eb/781516b808a99ddf288143cec46b342b3016c3414d137da1fdc3290d8860/pandas-3.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:f12b1a9e332c01e09510586f8ca9b108fd631fd656af82e452d7315ef6df5f9f", size = 9154922, upload-time = "2026-03-31T06:46:30.284Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b0/c20bd4d6d3f736e6bd6b55794e9cd0a617b858eaad27c8f410ea05d953b7/pandas-3.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:232a70ebb568c0c4d2db4584f338c1577d81e3af63292208d615907b698a0f18", size = 10347921, upload-time = "2026-03-31T06:46:33.36Z" }, + { url = "https://files.pythonhosted.org/packages/35/d0/4831af68ce30cc2d03c697bea8450e3225a835ef497d0d70f31b8cdde965/pandas-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:970762605cff1ca0d3f71ed4f3a769ea8f85fc8e6348f6e110b8fea7e6eb5a14", size = 9888127, upload-time = "2026-03-31T06:46:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/61/a9/16ea9346e1fc4a96e2896242d9bc674764fb9049b0044c0132502f7a771e/pandas-3.0.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aff4e6f4d722e0652707d7bcb190c445fe58428500c6d16005b02401764b1b3d", size = 10399577, upload-time = "2026-03-31T06:46:39.224Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a8/3a61a721472959ab0ce865ef05d10b0d6bfe27ce8801c99f33d4fa996e65/pandas-3.0.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef8b27695c3d3dc78403c9a7d5e59a62d5464a7e1123b4e0042763f7104dc74f", size = 10880030, upload-time = "2026-03-31T06:46:42.412Z" }, + { url = "https://files.pythonhosted.org/packages/da/65/7225c0ea4d6ce9cb2160a7fb7f39804871049f016e74782e5dade4d14109/pandas-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f8d68083e49e16b84734eb1a4dcae4259a75c90fb6e2251ab9a00b61120c06ab", size = 11409468, upload-time = "2026-03-31T06:46:45.2Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/46e7c76032639f2132359b5cf4c785dd8cf9aea5ea64699eac752f02b9db/pandas-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:32cc41f310ebd4a296d93515fcac312216adfedb1894e879303987b8f1e2b97d", size = 11936381, upload-time = "2026-03-31T06:46:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/7b/8b/721a9cff6fa6a91b162eb51019c6243b82b3226c71bb6c8ef4a9bd65cbc6/pandas-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:a4785e1d6547d8427c5208b748ae2efb64659a21bd82bf440d4262d02bfa02a4", size = 9744993, upload-time = "2026-03-31T06:46:51.488Z" }, + { url = "https://files.pythonhosted.org/packages/d5/18/7f0bd34ae27b28159aa80f2a6799f47fda34f7fb938a76e20c7b7fe3b200/pandas-3.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:08504503f7101300107ecdc8df73658e4347586db5cfdadabc1592e9d7e7a0fd", size = 9056118, upload-time = "2026-03-31T06:46:54.548Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ca/3e639a1ea6fcd0617ca4e8ca45f62a74de33a56ae6cd552735470b22c8d3/pandas-3.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5918ba197c951dec132b0c5929a00c0bf05d5942f590d3c10a807f6e15a57d3", size = 10321105, upload-time = "2026-03-31T06:46:57.327Z" }, + { url = "https://files.pythonhosted.org/packages/0b/77/dbc82ff2fb0e63c6564356682bf201edff0ba16c98630d21a1fb312a8182/pandas-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d606a041c89c0a474a4702d532ab7e73a14fe35c8d427b972a625c8e46373668", size = 9864088, upload-time = "2026-03-31T06:46:59.935Z" }, + { url = "https://files.pythonhosted.org/packages/5c/2b/341f1b04bbca2e17e13cd3f08c215b70ef2c60c5356ef1e8c6857449edc7/pandas-3.0.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:710246ba0616e86891b58ab95f2495143bb2bc83ab6b06747c74216f583a6ac9", size = 10369066, upload-time = "2026-03-31T06:47:02.792Z" }, + { url = "https://files.pythonhosted.org/packages/12/c5/cbb1ffefb20a93d3f0e1fdcda699fb84976210d411b008f97f48bf6ce27e/pandas-3.0.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d3cfe227c725b1f3dff4278b43d8c784656a42a9325b63af6b1492a8232209e", size = 10876780, upload-time = "2026-03-31T06:47:06.205Z" }, + { url = "https://files.pythonhosted.org/packages/98/fe/2249ae5e0a69bd0ddf17353d0a5d26611d70970111f5b3600cdc8be883e7/pandas-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c3b723df9087a9a9a840e263ebd9f88b64a12075d1bf2ea401a5a42f254f084d", size = 11375181, upload-time = "2026-03-31T06:47:09.383Z" }, + { url = "https://files.pythonhosted.org/packages/de/64/77a38b09e70b6464883b8d7584ab543e748e42c1b5d337a2ee088e0df741/pandas-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3096110bf9eac0070b7208465f2740e2d8a670d5cb6530b5bb884eca495fd39", size = 11928899, upload-time = "2026-03-31T06:47:12.686Z" }, + { url = "https://files.pythonhosted.org/packages/5e/52/42855bf626868413f761addd574acc6195880ae247a5346477a4361c3acb/pandas-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:07a10f5c36512eead51bc578eb3354ad17578b22c013d89a796ab5eee90cd991", size = 9746574, upload-time = "2026-03-31T06:47:15.64Z" }, + { url = "https://files.pythonhosted.org/packages/88/39/21304ae06a25e8bf9fc820d69b29b2c495b2ae580d1e143146c309941760/pandas-3.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:5fdbfa05931071aba28b408e59226186b01eb5e92bea2ab78b65863ca3228d84", size = 9047156, upload-time = "2026-03-31T06:47:18.595Z" }, + { url = "https://files.pythonhosted.org/packages/72/20/7defa8b27d4f330a903bb68eea33be07d839c5ea6bdda54174efcec0e1d2/pandas-3.0.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:dbc20dea3b9e27d0e66d74c42b2d0c1bed9c2ffe92adea33633e3bedeb5ac235", size = 10756238, upload-time = "2026-03-31T06:47:22.012Z" }, + { url = "https://files.pythonhosted.org/packages/e9/95/49433c14862c636afc0e9b2db83ff16b3ad92959364e52b2955e44c8e94c/pandas-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b75c347eff42497452116ce05ef461822d97ce5b9ff8df6edacb8076092c855d", size = 10408520, upload-time = "2026-03-31T06:47:25.197Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f8/462ad2b5881d6b8ec8e5f7ed2ea1893faa02290d13870a1600fe72ad8efc/pandas-3.0.2-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1478075142e83a5571782ad007fb201ed074bdeac7ebcc8890c71442e96adf7", size = 10324154, upload-time = "2026-03-31T06:47:28.097Z" }, + { url = "https://files.pythonhosted.org/packages/0a/65/d1e69b649cbcddda23ad6e4c40ef935340f6f652a006e5cbc3555ac8adb3/pandas-3.0.2-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5880314e69e763d4c8b27937090de570f1fb8d027059a7ada3f7f8e98bdcb677", size = 10714449, upload-time = "2026-03-31T06:47:30.85Z" }, + { url = "https://files.pythonhosted.org/packages/47/a4/85b59bc65b8190ea3689882db6cdf32a5003c0ccd5a586c30fdcc3ffc4fc/pandas-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b5329e26898896f06035241a626d7c335daa479b9bbc82be7c2742d048e41172", size = 11338475, upload-time = "2026-03-31T06:47:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c4/bc6966c6e38e5d9478b935272d124d80a589511ed1612a5d21d36f664c68/pandas-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:81526c4afd31971f8b62671442a4b2b51e0aa9acc3819c9f0f12a28b6fcf85f1", size = 11786568, upload-time = "2026-03-31T06:47:36.941Z" }, + { url = "https://files.pythonhosted.org/packages/e8/74/09298ca9740beed1d3504e073d67e128aa07e5ca5ca2824b0c674c0b8676/pandas-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:7cadd7e9a44ec13b621aec60f9150e744cfc7a3dd32924a7e2f45edff31823b0", size = 10488652, upload-time = "2026-03-31T06:47:40.612Z" }, + { url = "https://files.pythonhosted.org/packages/bb/40/c6ea527147c73b24fc15c891c3fcffe9c019793119c5742b8784a062c7db/pandas-3.0.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:db0dbfd2a6cdf3770aa60464d50333d8f3d9165b2f2671bcc299b72de5a6677b", size = 10326084, upload-time = "2026-03-31T06:47:43.834Z" }, + { url = "https://files.pythonhosted.org/packages/95/25/bdb9326c3b5455f8d4d3549fce7abcf967259de146fe2cf7a82368141948/pandas-3.0.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0555c5882688a39317179ab4a0ed41d3ebc8812ab14c69364bbee8fb7a3f6288", size = 9914146, upload-time = "2026-03-31T06:47:46.67Z" }, + { url = "https://files.pythonhosted.org/packages/8d/77/3a227ff3337aa376c60d288e1d61c5d097131d0ac71f954d90a8f369e422/pandas-3.0.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01f31a546acd5574ef77fe199bc90b55527c225c20ccda6601cf6b0fd5ed597c", size = 10444081, upload-time = "2026-03-31T06:47:49.681Z" }, + { url = "https://files.pythonhosted.org/packages/15/88/3cdd54fa279341afa10acf8d2b503556b1375245dccc9315659f795dd2e9/pandas-3.0.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:deeca1b5a931fdf0c2212c8a659ade6d3b1edc21f0914ce71ef24456ca7a6535", size = 10897535, upload-time = "2026-03-31T06:47:53.033Z" }, + { url = "https://files.pythonhosted.org/packages/06/9d/98cc7a7624f7932e40f434299260e2917b090a579d75937cb8a57b9d2de3/pandas-3.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f48afd9bb13300ffb5a3316973324c787054ba6665cda0da3fbd67f451995db", size = 11446992, upload-time = "2026-03-31T06:47:56.193Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cd/19ff605cc3760e80602e6826ddef2824d8e7050ed80f2e11c4b079741dc3/pandas-3.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6c4d8458b97a35717b62469a4ea0e85abd5ed8687277f5ccfc67f8a5126f8c53", size = 11968257, upload-time = "2026-03-31T06:47:59.137Z" }, + { url = "https://files.pythonhosted.org/packages/db/60/aba6a38de456e7341285102bede27514795c1eaa353bc0e7638b6b785356/pandas-3.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:b35d14bb5d8285d9494fe93815a9e9307c0876e10f1e8e89ac5b88f728ec8dcf", size = 9865893, upload-time = "2026-03-31T06:48:02.038Z" }, + { url = "https://files.pythonhosted.org/packages/08/71/e5ec979dd2e8a093dacb8864598c0ff59a0cee0bbcdc0bfec16a51684d4f/pandas-3.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:63d141b56ef686f7f0d714cfb8de4e320475b86bf4b620aa0b7da89af8cbdbbb", size = 9188644, upload-time = "2026-03-31T06:48:05.045Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6c/7b45d85db19cae1eb524f2418ceaa9d85965dcf7b764ed151386b7c540f0/pandas-3.0.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:140f0cffb1fa2524e874dde5b477d9defe10780d8e9e220d259b2c0874c89d9d", size = 10776246, upload-time = "2026-03-31T06:48:07.789Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3e/7b00648b086c106e81766f25322b48aa8dfa95b55e621dbdf2fdd413a117/pandas-3.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae37e833ff4fed0ba352f6bdd8b73ba3ab3256a85e54edfd1ab51ae40cca0af8", size = 10424801, upload-time = "2026-03-31T06:48:10.897Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/558dd09a71b53b4008e7fc8a98ec6d447e9bfb63cdaeea10e5eb9b2dabe8/pandas-3.0.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d888a5c678a419a5bb41a2a93818e8ed9fd3172246555c0b37b7cc27027effd", size = 10345643, upload-time = "2026-03-31T06:48:13.7Z" }, + { url = "https://files.pythonhosted.org/packages/be/e3/921c93b4d9a280409451dc8d07b062b503bbec0531d2627e73a756e99a82/pandas-3.0.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b444dc64c079e84df91baa8bf613d58405645461cabca929d9178f2cd392398d", size = 10743641, upload-time = "2026-03-31T06:48:16.659Z" }, + { url = "https://files.pythonhosted.org/packages/56/ca/fd17286f24fa3b4d067965d8d5d7e14fe557dd4f979a0b068ac0deaf8228/pandas-3.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4544c7a54920de8eeacaa1466a6b7268ecfbc9bc64ab4dbb89c6bbe94d5e0660", size = 11361993, upload-time = "2026-03-31T06:48:19.475Z" }, + { url = "https://files.pythonhosted.org/packages/e4/a5/2f6ed612056819de445a433ca1f2821ac3dab7f150d569a59e9cc105de1d/pandas-3.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:734be7551687c00fbd760dc0522ed974f82ad230d4a10f54bf51b80d44a08702", size = 11815274, upload-time = "2026-03-31T06:48:22.695Z" }, + { url = "https://files.pythonhosted.org/packages/00/2f/b622683e99ec3ce00b0854bac9e80868592c5b051733f2cf3a868e5fea26/pandas-3.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:57a07209bebcbcf768d2d13c9b78b852f9a15978dac41b9e6421a81ad4cdd276", size = 10888530, upload-time = "2026-03-31T06:48:25.806Z" }, + { url = "https://files.pythonhosted.org/packages/cb/2b/f8434233fab2bd66a02ec014febe4e5adced20e2693e0e90a07d118ed30e/pandas-3.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:5371b72c2d4d415d08765f32d689217a43227484e81b2305b52076e328f6f482", size = 9455341, upload-time = "2026-03-31T06:48:28.418Z" }, +] + [[package]] name = "pathspec" version = "0.12.1" @@ -949,6 +1108,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] +[[package]] +name = "tzdata" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, +] + [[package]] name = "urllib3" version = "2.5.0" From 20e531762cebb991183f601910cc5ca1d1cab1cf Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sun, 26 Apr 2026 16:53:32 -0700 Subject: [PATCH 186/267] Add unit tests for register api Co-authored-by: Copilot --- tests/fixtures.py | 3 - tests/protocol/test_register.py | 440 ++++++++++++++++++++++++++++++++ 2 files changed, 440 insertions(+), 3 deletions(-) create mode 100644 tests/protocol/test_register.py diff --git a/tests/fixtures.py b/tests/fixtures.py index d92f5f4..764c1b1 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -1,5 +1,3 @@ - - TIMESTAMP_1S: bytes = b"\x01\x00\x00\x00\x00\x00" @@ -12,7 +10,6 @@ def make_frame_from_raw( *, timestamp: bytes | None = None, ) -> bytes: - """Build a raw Harp frame from individual byte-level fields (for tests).""" ts = timestamp if timestamp is not None else b"" pt_byte = payload_type | (0x10 if ts else 0) body = bytes([address, port, pt_byte]) + ts + payload diff --git a/tests/protocol/test_register.py b/tests/protocol/test_register.py new file mode 100644 index 0000000..c487511 --- /dev/null +++ b/tests/protocol/test_register.py @@ -0,0 +1,440 @@ +"""Tests for _register.py and round-trips between register format/parse.""" + +from typing import ClassVar + +import numpy as np +import pytest +from harp.protocol._message import HarpMessage +from harp.protocol._message_type import MessageType +from harp.protocol._payload import ( + PayloadBase, + PayloadFloat, + PayloadS8, + PayloadS16, + PayloadS32, + PayloadS64, + PayloadU8, + PayloadU16, + PayloadU32, + PayloadU64, +) +from harp.protocol._payload_type import PayloadType +from harp.protocol._register import ( + RegisterBase, + RegisterFloat, + RegisterS8, + RegisterS16, + RegisterS16Array, + RegisterS32, + RegisterS64, + RegisterU8, + RegisterU16, + RegisterU32, + RegisterU32Array, + RegisterU64, +) +from numpy.typing import NDArray + +# --------------------------------------------------------------------------- +# Fixtures / helpers +# --------------------------------------------------------------------------- + + +class TimestampSecond(RegisterU32): + address: ClassVar[int] = 8 + + +class DigitalOutputSet(RegisterU16): + address: ClassVar[int] = 32 + + +class AnalogDataPayload(PayloadBase): + _dtype: ClassVar = np.dtype( + [ + ("analog_input0", " NDArray[np.int16]: + return self._arr["analog_input0"] + + @property + def encoder(self) -> NDArray[np.int16]: + return self._arr["encoder"] + + @property + def analog_input1(self) -> NDArray[np.int16]: + return self._arr["analog_input1"] + + +class AnalogData(RegisterBase[AnalogDataPayload]): + address: ClassVar[int] = 33 + payload_type: ClassVar[PayloadType] = PayloadType.S16 + payload_class: ClassVar = AnalogDataPayload + + +def _parse_frame(frame: bytes) -> HarpMessage: + return HarpMessage.parse(frame) + + +@pytest.mark.parametrize( + "reg_cls, address, payload_type, value, dtype", + [ + (RegisterU8, 0x10, PayloadType.U8, 42, np.dtype("u1")), + (RegisterU16, 0x11, PayloadType.U16, 1000, np.dtype(" len == 1 => .value returns arr[0], shape (3,) + assert len(parsed) == 1 + v = parsed.value + assert isinstance(v, np.ndarray) + assert v.shape == (3,) + assert int(v[0]) == 10 + assert int(v[1]) == 20 + assert int(v[2]) == 30 + np.testing.assert_array_equal(v, values) + + +def test_array_register_value_multi(): + """.value on a multi-record array-register payload returns the full 2-D array.""" + reg = RegisterU32Array(0x08, length=3) + # Two rows of 3 elements each; pass as flat bytes via parse_bulk + rows = np.array([[10, 20, 30], [40, 50, 60]], dtype=np.dtype(" len == 2 => .value returns the full array + assert len(bulk) == 2 + v = bulk.value + assert isinstance(v, np.ndarray) + assert v.shape == (2, 3) + np.testing.assert_array_equal(v[0], [10, 20, 30]) + np.testing.assert_array_equal(v[1], [40, 50, 60]) From 1a70a59e19c4de99e76d2cfcf71490bdb207d25f Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sun, 26 Apr 2026 17:20:15 -0700 Subject: [PATCH 187/267] Improve string visualization for Payload Co-authored-by: Copilot --- src/harp-device/src/harp/device/_registers.py | 13 +++++++++++ src/harp-protocol/harp/protocol/_payload.py | 23 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/harp-device/src/harp/device/_registers.py b/src/harp-device/src/harp/device/_registers.py index df179d1..7743250 100644 --- a/src/harp-device/src/harp/device/_registers.py +++ b/src/harp-device/src/harp/device/_registers.py @@ -18,6 +18,14 @@ class OperationMode(enum.IntEnum): class OperationControlPayload(PayloadBase[np.void]): _dtype: ClassVar = np.dtype([("operation_control", "u1")]) + _repr_fields: ClassVar = ( + "operation_mode", + "dump_registers", + "mute_replies", + "visual_indicators", + "operation_led", + "heartbeat", + ) def __init__( self, @@ -142,3 +150,8 @@ class SerialNumber(RegisterU16): class TimestampOffset(RegisterU8): address: ClassVar[int] = 15 + address: ClassVar[int] = 15 + address: ClassVar[int] = 15 + address: ClassVar[int] = 15 + address: ClassVar[int] = 15 + address: ClassVar[int] = 15 diff --git a/src/harp-protocol/harp/protocol/_payload.py b/src/harp-protocol/harp/protocol/_payload.py index 8c78238..8ebaeb9 100644 --- a/src/harp-protocol/harp/protocol/_payload.py +++ b/src/harp-protocol/harp/protocol/_payload.py @@ -22,9 +22,15 @@ class AnalogDataPayload(PayloadBase): ("analog_input0", " None: @@ -92,6 +98,23 @@ def to_dataframe(self) -> pd.DataFrame: def __len__(self) -> int: return len(self._arr) + def _repr_kwargs(self) -> str: + """Return the ``key=value`` portion used by ``__repr__`` and ``__str__``.""" + fields: tuple[str, ...] + if self._repr_fields is not None: + fields = self._repr_fields + elif self._dtype.names is not None: + fields = self._dtype.names + else: + return f"value={self.value!r}" + return ", ".join(f"{f}={getattr(self, f)!r}" for f in fields) + + def __repr__(self) -> str: + return f"{type(self).__name__}({self._repr_kwargs()})" + + def __str__(self) -> str: + return repr(self) + # ------------------------------------------------------------------ # Named scalar payload classes — one per PayloadType. From 39b99041503af77fc85fdef24d2e2183b12fd3e4 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sun, 26 Apr 2026 17:20:22 -0700 Subject: [PATCH 188/267] Add small integration test --- device_integration.py | 98 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 device_integration.py diff --git a/device_integration.py b/device_integration.py new file mode 100644 index 0000000..cc8e9f0 --- /dev/null +++ b/device_integration.py @@ -0,0 +1,98 @@ +import time + +import numpy as np +from harp.device import ( + AssemblyVersion, + ClockConfig, + CoreVersionH, + CoreVersionL, + Device, + FirmwareVersionH, + FirmwareVersionL, + Heartbeat, + HwVersionH, + HwVersionL, + OperationControl, + ResetDevice, + SerialNumber, + TimestampMicro, + TimestampSecond, + WhoAmI, +) + +PORT = "COM3" +N_READS = 10_000 + +CORE_REGISTERS = [ + ("WhoAmI", WhoAmI), + ("HwVersionH", HwVersionH), + ("HwVersionL", HwVersionL), + ("AssemblyVersion", AssemblyVersion), + ("CoreVersionH", CoreVersionH), + ("CoreVersionL", CoreVersionL), + ("FirmwareVersionH", FirmwareVersionH), + ("FirmwareVersionL", FirmwareVersionL), + ("TimestampSecond", TimestampSecond), + ("TimestampMicro", TimestampMicro), + ("OperationControl", OperationControl), + ("ResetDevice", ResetDevice), + ("SerialNumber", SerialNumber), + ("ClockConfig", ClockConfig), + ( + "Heartbeat", + Heartbeat, + ), # TODO This is erroring out in pico devices. May be something with the serial class +] + + +def read_core_registers(dev: Device) -> None: + print("\n=== Core register snapshot ===") + print(f"{'Register':<20} {'Address':>7} {'Value'}") + print("-" * 50) + for name, reg in CORE_REGISTERS: + msg = dev.read(reg) + print(f"{name:<20} {reg.address} {msg.parsed}") + + # TODO just noticed the alias properties inside complex structs payloads like OperationControlPayload + # are returning [value] instead of value. Prob decorate them with something and keep the internal array representation + # hidden internally? + + +def whoa_latency_benchmark(dev: Device, n: int = N_READS) -> None: + print(f"\n=== WhoAmI round-trip benchmark (n={n:,}) ===") + print("Collecting timestamps …") + + t0 = time.perf_counter() + timestamps = np.empty(n, dtype=np.float64) + for i in range(n): + msg = dev.read(WhoAmI) + timestamps[i] = msg.timestamp + elapsed = time.perf_counter() - t0 + + ts = np.array(timestamps, dtype=np.float64) + if len(ts) < 2: + print("Not enough timestamped replies to compute statistics.") + return + + # Latency proxy: delta between successive device timestamps + deltas_ms = np.diff(ts) * 1e3 # seconds → milliseconds + + print(f"\n Total wall time : {elapsed:.2f} s ({elapsed / n * 1e3:.2f} ms/read)") + print(f" Replies with TS : {len(ts):,} / {n:,}") + print(f"\n Inter-reply delta (ms) [n={len(deltas_ms):,}]") + print(f" min : {deltas_ms.min():.3f}") + print(f" p1 : {np.percentile(deltas_ms, 1):.3f}") + print(f" mean : {deltas_ms.mean():.3f}") + print(f" std : {deltas_ms.std():.3f}") + print(f" p99 : {np.percentile(deltas_ms, 99):.3f}") + print(f" max : {deltas_ms.max():.3f}") + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + +if __name__ == "__main__": + with Device(PORT) as dev: + read_core_registers(dev) + whoa_latency_benchmark(dev) From c070cf870ad3e39df6b199949cf68896d4291543 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sun, 26 Apr 2026 19:13:25 -0700 Subject: [PATCH 189/267] Remove data type inference Co-authored-by: Copilot --- src/harp-protocol/harp/protocol/_payload.py | 28 +++++++------------- src/harp-protocol/harp/protocol/_register.py | 15 +++++------ 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/src/harp-protocol/harp/protocol/_payload.py b/src/harp-protocol/harp/protocol/_payload.py index 8ebaeb9..85d365a 100644 --- a/src/harp-protocol/harp/protocol/_payload.py +++ b/src/harp-protocol/harp/protocol/_payload.py @@ -1,18 +1,15 @@ from __future__ import annotations -from typing import TYPE_CHECKING, ClassVar, Generic, Self, TypeVar, final +from typing import ClassVar, Generic, Self, TypeVar, final import numpy as np import pandas as pd from numpy.typing import NDArray -if TYPE_CHECKING: - from ._payload_type import PayloadType +NpStructT = TypeVar("NpStructT", bound=np.generic | np.ndarray) -ScalarT = TypeVar("ScalarT", bound=np.generic | np.ndarray) - -class PayloadBase(Generic[ScalarT]): +class PayloadBase(Generic[NpStructT]): """Base class for typed Harp register payloads. Subclasses define ``_dtype: ClassVar[np.dtype]`` for the register layout:: @@ -43,13 +40,17 @@ def __init__(self, *args: object, **kwargs: object) -> None: ) unknown = set(kwargs) - set(self._dtype.names) if unknown: - raise TypeError(f"{type(self).__name__}() got unexpected kwargs: {sorted(unknown)}") + raise TypeError( + f"{type(self).__name__}() got unexpected kwargs: {sorted(unknown)}" + ) values = tuple(kwargs[n] for n in self._dtype.names) self._arr = np.array([values], dtype=self._dtype) else: # Scalar dtype — single positional value if len(args) != 1 or kwargs: - raise TypeError(f"{type(self).__name__}() takes exactly one positional argument") + raise TypeError( + f"{type(self).__name__}() takes exactly one positional argument" + ) self._arr = np.array([args[0]], dtype=self._dtype) def __init_subclass__(cls, **kwargs: object) -> None: @@ -60,15 +61,6 @@ def __init_subclass__(cls, **kwargs: object) -> None: if not isinstance(raw, np.dtype): cls._dtype = np.dtype(raw) - @classmethod - def scalar(cls, payload_type: "PayloadType") -> type[PayloadBase]: - """Return a dynamically-generated ``PayloadBase`` subclass for a scalar type.""" - ## TODO we should prob consider removing this and just require explicit payload classes for all registers, even scalars. It's only a few lines of boilerplate to define a new one, and it would simplify the codebase by eliminating this dynamic class generation logic. - - dtype = payload_type.numpy_dtype - name = f"_Scalar{payload_type.name}Payload" - return type(name, (PayloadBase,), {"_dtype": dtype}) - @classmethod def from_buffer(cls, buf: bytes | bytearray | memoryview) -> Self: """Construct from a raw byte buffer interpreted as an array of ``_dtype`` records.""" @@ -78,7 +70,7 @@ def from_buffer(cls, buf: bytes | bytearray | memoryview) -> Self: return obj @property - def value(self) -> ScalarT: + def value(self) -> NpStructT: """Returns a single scalar if the array has one element, otherwise the full array.""" if len(self._arr) == 1: return self._arr[0] # type: ignore[return-value] diff --git a/src/harp-protocol/harp/protocol/_register.py b/src/harp-protocol/harp/protocol/_register.py index 4965bdf..aeb558e 100644 --- a/src/harp-protocol/harp/protocol/_register.py +++ b/src/harp-protocol/harp/protocol/_register.py @@ -71,12 +71,6 @@ class RegisterBase(ABC, Generic[P]): payload_class: ClassVar[type[Any]] length: ClassVar[int | None] = None - def __init_subclass__(cls, **kwargs: object) -> None: - super().__init_subclass__(**kwargs) - # Auto-generate scalar payload class if not explicitly defined - if "payload_class" not in cls.__dict__ and "payload_type" in cls.__dict__: - cls.payload_class = PayloadBase.scalar(cls.payload_type) - @classmethod def parse(cls, value: HarpMessage | bytes | bytearray | memoryview) -> P: # type: ignore[type-var] """Parse the payload from a ``HarpMessage`` or raw bytes.""" @@ -127,13 +121,18 @@ def format( if isinstance(value, PayloadBase): # Payload instance — use its backing array bytes directly raw = value._arr.tobytes() - elif isinstance(value, np.ndarray) and value.dtype != cls.payload_type.numpy_dtype: + elif ( + isinstance(value, np.ndarray) + and value.dtype != cls.payload_type.numpy_dtype + ): # Structured numpy array passed by hand raw = value.tobytes() else: # Scalar or array castable to the register's primitive dtype raw = np.asarray(value, dtype=cls.payload_type.numpy_dtype).tobytes() - return build_message_frame(mt, cls.address, cls.payload_type, raw, port=port) + return build_message_frame( + mt, cls.address, cls.payload_type, raw, port=port + ) # ------------------------------------------------------------------ From 92cf83d21ebf59b975462437eed7827b931036a5 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sun, 26 Apr 2026 19:13:46 -0700 Subject: [PATCH 190/267] Ignore .vscode folder Co-authored-by: Copilot --- .gitignore | 3 +++ .vscode/extensions.json | 7 ------- .vscode/settings.json | 15 --------------- 3 files changed, 3 insertions(+), 22 deletions(-) delete mode 100644 .vscode/extensions.json delete mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index 0a19790..f6cc12e 100644 --- a/.gitignore +++ b/.gitignore @@ -172,3 +172,6 @@ cython_debug/ # PyPI configuration file .pypirc + +# vscode +.vscode/ \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index dd4b3f2..0000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "recommendations": [ - "ms-python.python", - "charliermarsh.ruff", - "meta.pyrefly" - ] -} diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 3ad43fe..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "editor.defaultFormatter": "charliermarsh.ruff", - "editor.formatOnSave": true, - "editor.formatOnPaste": true, - "ruff.organizeImports": true, - "editor.codeActionsOnSave": { - "source.organizeImports": "explicit" - }, - "python.testing.pytestArgs": [ - "tests" - ], - "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true, - "python.pyrefly.displayTypeErrors": "force-on", -} \ No newline at end of file From 66987d1c4a4b404bac265926944ea61b6e95ca58 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sun, 26 Apr 2026 19:14:17 -0700 Subject: [PATCH 191/267] Linting Co-authored-by: Copilot --- src/harp-device/src/harp/device/_registers.py | 21 ++++++++----------- src/harp-protocol/harp/protocol/_payload.py | 8 ++----- src/harp-protocol/harp/protocol/_register.py | 9 ++------ 3 files changed, 13 insertions(+), 25 deletions(-) diff --git a/src/harp-device/src/harp/device/_registers.py b/src/harp-device/src/harp/device/_registers.py index 7743250..2744988 100644 --- a/src/harp-device/src/harp/device/_registers.py +++ b/src/harp-device/src/harp/device/_registers.py @@ -38,12 +38,12 @@ def __init__( heartbeat: int | np.uint8 = 0, ) -> None: arr = np.zeros(1, dtype=self._dtype) - arr["operation_control"] |= np.uint8(operation_mode) & np.uint8(0x03) - arr["operation_control"] |= np.uint8(dump_registers) << np.uint8(3) - arr["operation_control"] |= np.uint8(mute_replies) << np.uint8(4) - arr["operation_control"] |= (np.uint8(visual_indicators) & np.uint8(0x01)) << np.uint8(5) - arr["operation_control"] |= (np.uint8(operation_led) & np.uint8(0x01)) << np.uint8(6) - arr["operation_control"] |= (np.uint8(heartbeat) & np.uint8(0x01)) << np.uint8(7) + arr["operation_control"] |= np.uint8(operation_mode) & 0x03 + arr["operation_control"] |= np.uint8(dump_registers) << 3 + arr["operation_control"] |= np.uint8(mute_replies) << 4 + arr["operation_control"] |= (np.uint8(visual_indicators) & 0x01) << 5 + arr["operation_control"] |= (np.uint8(operation_led) & 0x01) << 6 + arr["operation_control"] |= (np.uint8(heartbeat) & 0x01) << 7 self._arr = arr @property @@ -60,15 +60,15 @@ def mute_replies(self) -> NDArray[np.bool_]: @property def visual_indicators(self) -> NDArray[np.bool_]: - return ((self._arr["operation_control"] >> np.uint8(5)) & np.uint8(0x01)) != 0 + return ((self._arr["operation_control"] >> 5) & np.uint8(0x01)) != 0 @property def operation_led(self) -> NDArray[np.bool_]: - return ((self._arr["operation_control"] >> np.uint8(6)) & np.uint8(0x01)) != 0 + return ((self._arr["operation_control"] >> 6) & np.uint8(0x01)) != 0 @property def heartbeat(self) -> NDArray[np.bool_]: - return ((self._arr["operation_control"] >> np.uint8(7)) & np.uint8(0x01)) != 0 + return ((self._arr["operation_control"] >> 7) & np.uint8(0x01)) != 0 def to_dataframe(self) -> pd.DataFrame: return pd.DataFrame( @@ -152,6 +152,3 @@ class TimestampOffset(RegisterU8): address: ClassVar[int] = 15 address: ClassVar[int] = 15 address: ClassVar[int] = 15 - address: ClassVar[int] = 15 - address: ClassVar[int] = 15 - address: ClassVar[int] = 15 diff --git a/src/harp-protocol/harp/protocol/_payload.py b/src/harp-protocol/harp/protocol/_payload.py index 85d365a..b451496 100644 --- a/src/harp-protocol/harp/protocol/_payload.py +++ b/src/harp-protocol/harp/protocol/_payload.py @@ -40,17 +40,13 @@ def __init__(self, *args: object, **kwargs: object) -> None: ) unknown = set(kwargs) - set(self._dtype.names) if unknown: - raise TypeError( - f"{type(self).__name__}() got unexpected kwargs: {sorted(unknown)}" - ) + raise TypeError(f"{type(self).__name__}() got unexpected kwargs: {sorted(unknown)}") values = tuple(kwargs[n] for n in self._dtype.names) self._arr = np.array([values], dtype=self._dtype) else: # Scalar dtype — single positional value if len(args) != 1 or kwargs: - raise TypeError( - f"{type(self).__name__}() takes exactly one positional argument" - ) + raise TypeError(f"{type(self).__name__}() takes exactly one positional argument") self._arr = np.array([args[0]], dtype=self._dtype) def __init_subclass__(cls, **kwargs: object) -> None: diff --git a/src/harp-protocol/harp/protocol/_register.py b/src/harp-protocol/harp/protocol/_register.py index aeb558e..e1ac9e6 100644 --- a/src/harp-protocol/harp/protocol/_register.py +++ b/src/harp-protocol/harp/protocol/_register.py @@ -121,18 +121,13 @@ def format( if isinstance(value, PayloadBase): # Payload instance — use its backing array bytes directly raw = value._arr.tobytes() - elif ( - isinstance(value, np.ndarray) - and value.dtype != cls.payload_type.numpy_dtype - ): + elif isinstance(value, np.ndarray) and value.dtype != cls.payload_type.numpy_dtype: # Structured numpy array passed by hand raw = value.tobytes() else: # Scalar or array castable to the register's primitive dtype raw = np.asarray(value, dtype=cls.payload_type.numpy_dtype).tobytes() - return build_message_frame( - mt, cls.address, cls.payload_type, raw, port=port - ) + return build_message_frame(mt, cls.address, cls.payload_type, raw, port=port) # ------------------------------------------------------------------ From 3d4cbd4f6d5ad6132d4639bec50fc4de8d64a491 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sun, 26 Apr 2026 19:17:49 -0700 Subject: [PATCH 192/267] Remove unused type checking guards Co-authored-by: Copilot --- src/harp-protocol/harp/protocol/_register.py | 38 ++++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/harp-protocol/harp/protocol/_register.py b/src/harp-protocol/harp/protocol/_register.py index e1ac9e6..ab3459d 100644 --- a/src/harp-protocol/harp/protocol/_register.py +++ b/src/harp-protocol/harp/protocol/_register.py @@ -143,47 +143,47 @@ def format( # ------------------------------------------------------------------ -class RegisterU8(RegisterBase[PayloadU8], metaclass=_RegisterMeta): # type: ignore[misc] +class RegisterU8(RegisterBase[PayloadU8], metaclass=_RegisterMeta): payload_type: ClassVar[PayloadType] = PayloadType.U8 payload_class: ClassVar[type[PayloadU8]] = PayloadU8 -class RegisterU16(RegisterBase[PayloadU16], metaclass=_RegisterMeta): # type: ignore[misc] +class RegisterU16(RegisterBase[PayloadU16], metaclass=_RegisterMeta): payload_type: ClassVar[PayloadType] = PayloadType.U16 payload_class: ClassVar[type[PayloadU16]] = PayloadU16 -class RegisterU32(RegisterBase[PayloadU32], metaclass=_RegisterMeta): # type: ignore[misc] +class RegisterU32(RegisterBase[PayloadU32], metaclass=_RegisterMeta): payload_type: ClassVar[PayloadType] = PayloadType.U32 payload_class: ClassVar[type[PayloadU32]] = PayloadU32 -class RegisterU64(RegisterBase[PayloadU64], metaclass=_RegisterMeta): # type: ignore[misc] +class RegisterU64(RegisterBase[PayloadU64], metaclass=_RegisterMeta): payload_type: ClassVar[PayloadType] = PayloadType.U64 payload_class: ClassVar[type[PayloadU64]] = PayloadU64 -class RegisterS8(RegisterBase[PayloadS8], metaclass=_RegisterMeta): # type: ignore[misc] +class RegisterS8(RegisterBase[PayloadS8], metaclass=_RegisterMeta): payload_type: ClassVar[PayloadType] = PayloadType.S8 payload_class: ClassVar[type[PayloadS8]] = PayloadS8 -class RegisterS16(RegisterBase[PayloadS16], metaclass=_RegisterMeta): # type: ignore[misc] +class RegisterS16(RegisterBase[PayloadS16], metaclass=_RegisterMeta): payload_type: ClassVar[PayloadType] = PayloadType.S16 payload_class: ClassVar[type[PayloadS16]] = PayloadS16 -class RegisterS32(RegisterBase[PayloadS32], metaclass=_RegisterMeta): # type: ignore[misc] +class RegisterS32(RegisterBase[PayloadS32], metaclass=_RegisterMeta): payload_type: ClassVar[PayloadType] = PayloadType.S32 payload_class: ClassVar[type[PayloadS32]] = PayloadS32 -class RegisterS64(RegisterBase[PayloadS64], metaclass=_RegisterMeta): # type: ignore[misc] +class RegisterS64(RegisterBase[PayloadS64], metaclass=_RegisterMeta): payload_type: ClassVar[PayloadType] = PayloadType.S64 payload_class: ClassVar[type[PayloadS64]] = PayloadS64 -class RegisterFloat(RegisterBase[PayloadFloat], metaclass=_RegisterMeta): # type: ignore[misc] +class RegisterFloat(RegisterBase[PayloadFloat], metaclass=_RegisterMeta): payload_type: ClassVar[PayloadType] = PayloadType.Float payload_class: ClassVar[type[PayloadFloat]] = PayloadFloat @@ -206,7 +206,7 @@ class RegisterFloat(RegisterBase[PayloadFloat], metaclass=_RegisterMeta): # typ class _ArrayRegisterMeta(ABCMeta): """Calling with address and length creates a concrete subclass: ``RegisterU16Array(0x28, length=3)``.""" - def __call__(cls: "type[_AR]", address: int, *, length: int) -> "type[_AR]": # type: ignore[override] + def __call__(cls: "type[_AR]", address: int, *, length: int) -> "type[_AR]": # type: ignore[override, misc] base_payload = cls.payload_class # type: ignore[attr-defined] concrete_payload = type( f"{base_payload.__name__}_{length}", @@ -227,46 +227,46 @@ def __call__(cls: "type[_AR]", address: int, *, length: int) -> "type[_AR]": # ) -class RegisterU8Array(RegisterBase[PayloadU8Array], metaclass=_ArrayRegisterMeta): # type: ignore[misc] +class RegisterU8Array(RegisterBase[PayloadU8Array], metaclass=_ArrayRegisterMeta): payload_type: ClassVar[PayloadType] = PayloadType.U8 payload_class: ClassVar[type[Any]] = PayloadU8Array -class RegisterU16Array(RegisterBase[PayloadU16Array], metaclass=_ArrayRegisterMeta): # type: ignore[misc] +class RegisterU16Array(RegisterBase[PayloadU16Array], metaclass=_ArrayRegisterMeta): payload_type: ClassVar[PayloadType] = PayloadType.U16 payload_class: ClassVar[type[Any]] = PayloadU16Array -class RegisterU32Array(RegisterBase[PayloadU32Array], metaclass=_ArrayRegisterMeta): # type: ignore[misc] +class RegisterU32Array(RegisterBase[PayloadU32Array], metaclass=_ArrayRegisterMeta): payload_type: ClassVar[PayloadType] = PayloadType.U32 payload_class: ClassVar[type[Any]] = PayloadU32Array -class RegisterU64Array(RegisterBase[PayloadU64Array], metaclass=_ArrayRegisterMeta): # type: ignore[misc] +class RegisterU64Array(RegisterBase[PayloadU64Array], metaclass=_ArrayRegisterMeta): payload_type: ClassVar[PayloadType] = PayloadType.U64 payload_class: ClassVar[type[Any]] = PayloadU64Array -class RegisterS8Array(RegisterBase[PayloadS8Array], metaclass=_ArrayRegisterMeta): # type: ignore[misc] +class RegisterS8Array(RegisterBase[PayloadS8Array], metaclass=_ArrayRegisterMeta): payload_type: ClassVar[PayloadType] = PayloadType.S8 payload_class: ClassVar[type[Any]] = PayloadS8Array -class RegisterS16Array(RegisterBase[PayloadS16Array], metaclass=_ArrayRegisterMeta): # type: ignore[misc] +class RegisterS16Array(RegisterBase[PayloadS16Array], metaclass=_ArrayRegisterMeta): payload_type: ClassVar[PayloadType] = PayloadType.S16 payload_class: ClassVar[type[Any]] = PayloadS16Array -class RegisterS32Array(RegisterBase[PayloadS32Array], metaclass=_ArrayRegisterMeta): # type: ignore[misc] +class RegisterS32Array(RegisterBase[PayloadS32Array], metaclass=_ArrayRegisterMeta): payload_type: ClassVar[PayloadType] = PayloadType.S32 payload_class: ClassVar[type[Any]] = PayloadS32Array -class RegisterS64Array(RegisterBase[PayloadS64Array], metaclass=_ArrayRegisterMeta): # type: ignore[misc] +class RegisterS64Array(RegisterBase[PayloadS64Array], metaclass=_ArrayRegisterMeta): payload_type: ClassVar[PayloadType] = PayloadType.S64 payload_class: ClassVar[type[Any]] = PayloadS64Array -class RegisterFloatArray(RegisterBase[PayloadFloatArray], metaclass=_ArrayRegisterMeta): # type: ignore[misc] +class RegisterFloatArray(RegisterBase[PayloadFloatArray], metaclass=_ArrayRegisterMeta): payload_type: ClassVar[PayloadType] = PayloadType.Float payload_class: ClassVar[type[Any]] = PayloadFloatArray From 53c4e73deb2ae0fcc4a31b403c77f2be2ccb0354 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sun, 26 Apr 2026 19:28:57 -0700 Subject: [PATCH 193/267] Improve type hinting Co-authored-by: Copilot --- src/harp-protocol/harp/protocol/_register.py | 8 +- uv.lock | 792 +++++++++++-------- 2 files changed, 458 insertions(+), 342 deletions(-) diff --git a/src/harp-protocol/harp/protocol/_register.py b/src/harp-protocol/harp/protocol/_register.py index ab3459d..b4b94b8 100644 --- a/src/harp-protocol/harp/protocol/_register.py +++ b/src/harp-protocol/harp/protocol/_register.py @@ -52,7 +52,7 @@ class TimestampSecond(RegisterU32): class _RegisterMeta(ABCMeta): """Calling a register class with an address creates a one-off subclass: ``RegisterU32(0x08)``.""" - def __call__(cls: "type[_R]", address: int) -> "type[_R]": # type: ignore[override] + def __call__(cls: "type[_R]", address: int) -> "type[_R]": return cast( "type[_R]", type(f"{cls.__name__}_{address:#04x}", (cls,), {"address": address}), @@ -68,21 +68,21 @@ class RegisterBase(ABC, Generic[P]): address: ClassVar[int] payload_type: ClassVar[PayloadType] - payload_class: ClassVar[type[Any]] + payload_class: ClassVar[type[Any]] # Gotta keep Any so children can override without warning. length: ClassVar[int | None] = None @classmethod def parse(cls, value: HarpMessage | bytes | bytearray | memoryview) -> P: # type: ignore[type-var] """Parse the payload from a ``HarpMessage`` or raw bytes.""" buf = value.payload if isinstance(value, HarpMessage) else value - return cls.payload_class.from_buffer(buf) + return cast(P, cls.payload_class.from_buffer(buf)) @classmethod def parse_bulk(cls, data: bytes | bytearray | memoryview | list[HarpMessage]) -> P: # type: ignore[type-var] """Parse a batch of payloads from concatenated bytes or a list of messages.""" if isinstance(data, list): data = b"".join(msg.payload for msg in data) - return cls.payload_class.from_buffer(data) + return cast(P, cls.payload_class.from_buffer(data)) @overload @classmethod diff --git a/uv.lock b/uv.lock index be04bd4..4d72262 100644 --- a/uv.lock +++ b/uv.lock @@ -19,25 +19,25 @@ members = [ [[package]] name = "babel" -version = "2.17.0" +version = "2.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, ] [[package]] name = "backrefs" -version = "5.9" +version = "6.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/eb/a7/312f673df6a79003279e1f55619abbe7daebbb87c17c976ddc0345c04c7b/backrefs-5.9.tar.gz", hash = "sha256:808548cb708d66b82ee231f962cb36faaf4f2baab032f2fbb783e9c2fdddaa59", size = 5765857, upload-time = "2025-06-22T19:34:13.97Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/a6/e325ec73b638d3ede4421b5445d4a0b8b219481826cc079d510100af356c/backrefs-6.2.tar.gz", hash = "sha256:f44ff4d48808b243b6c0cdc6231e22195c32f77046018141556c66f8bab72a49", size = 7012303, upload-time = "2026-02-16T19:10:15.828Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/4d/798dc1f30468134906575156c089c492cf79b5a5fd373f07fe26c4d046bf/backrefs-5.9-py310-none-any.whl", hash = "sha256:db8e8ba0e9de81fcd635f440deab5ae5f2591b54ac1ebe0550a2ca063488cd9f", size = 380267, upload-time = "2025-06-22T19:34:05.252Z" }, - { url = "https://files.pythonhosted.org/packages/55/07/f0b3375bf0d06014e9787797e6b7cc02b38ac9ff9726ccfe834d94e9991e/backrefs-5.9-py311-none-any.whl", hash = "sha256:6907635edebbe9b2dc3de3a2befff44d74f30a4562adbb8b36f21252ea19c5cf", size = 392072, upload-time = "2025-06-22T19:34:06.743Z" }, - { url = "https://files.pythonhosted.org/packages/9d/12/4f345407259dd60a0997107758ba3f221cf89a9b5a0f8ed5b961aef97253/backrefs-5.9-py312-none-any.whl", hash = "sha256:7fdf9771f63e6028d7fee7e0c497c81abda597ea45d6b8f89e8ad76994f5befa", size = 397947, upload-time = "2025-06-22T19:34:08.172Z" }, - { url = "https://files.pythonhosted.org/packages/10/bf/fa31834dc27a7f05e5290eae47c82690edc3a7b37d58f7fb35a1bdbf355b/backrefs-5.9-py313-none-any.whl", hash = "sha256:cc37b19fa219e93ff825ed1fed8879e47b4d89aa7a1884860e2db64ccd7c676b", size = 399843, upload-time = "2025-06-22T19:34:09.68Z" }, - { url = "https://files.pythonhosted.org/packages/fc/24/b29af34b2c9c41645a9f4ff117bae860291780d73880f449e0b5d948c070/backrefs-5.9-py314-none-any.whl", hash = "sha256:df5e169836cc8acb5e440ebae9aad4bf9d15e226d3bad049cf3f6a5c20cc8dc9", size = 411762, upload-time = "2025-06-22T19:34:11.037Z" }, - { url = "https://files.pythonhosted.org/packages/41/ff/392bff89415399a979be4a65357a41d92729ae8580a66073d8ec8d810f98/backrefs-5.9-py39-none-any.whl", hash = "sha256:f48ee18f6252b8f5777a22a00a09a85de0ca931658f1dd96d4406a34f3748c60", size = 380265, upload-time = "2025-06-22T19:34:12.405Z" }, + { url = "https://files.pythonhosted.org/packages/1b/39/3765df263e08a4df37f4f43cb5aa3c6c17a4bdd42ecfe841e04c26037171/backrefs-6.2-py310-none-any.whl", hash = "sha256:0fdc7b012420b6b144410342caeb8adc54c6866cf12064abc9bb211302e496f8", size = 381075, upload-time = "2026-02-16T19:10:04.322Z" }, + { url = "https://files.pythonhosted.org/packages/0f/f0/35240571e1b67ffb19dafb29ab34150b6f59f93f717b041082cdb1bfceb1/backrefs-6.2-py311-none-any.whl", hash = "sha256:08aa7fae530c6b2361d7bdcbda1a7c454e330cc9dbcd03f5c23205e430e5c3be", size = 392874, upload-time = "2026-02-16T19:10:06.314Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/77e8c9745b4d227cce9f5e0a6f68041278c5f9b18588b35905f5f19c1beb/backrefs-6.2-py312-none-any.whl", hash = "sha256:c3f4b9cb2af8cda0d87ab4f57800b57b95428488477be164dd2b47be54db0c90", size = 398787, upload-time = "2026-02-16T19:10:08.274Z" }, + { url = "https://files.pythonhosted.org/packages/c5/71/c754b1737ad99102e03fa3235acb6cb6d3ac9d6f596cbc3e5f236705abd8/backrefs-6.2-py313-none-any.whl", hash = "sha256:12df81596ab511f783b7d87c043ce26bc5b0288cf3bb03610fe76b8189282b2b", size = 400747, upload-time = "2026-02-16T19:10:09.791Z" }, + { url = "https://files.pythonhosted.org/packages/af/75/be12ba31a6eb20dccef2320cd8ccb3f7d9013b68ba4c70156259fee9e409/backrefs-6.2-py314-none-any.whl", hash = "sha256:e5f805ae09819caa1aa0623b4a83790e7028604aa2b8c73ba602c4454e665de7", size = 412602, upload-time = "2026-02-16T19:10:12.317Z" }, + { url = "https://files.pythonhosted.org/packages/21/f8/d02f650c47d05034dcd6f9c8cf94f39598b7a89c00ecda0ecb2911bc27e9/backrefs-6.2-py39-none-any.whl", hash = "sha256:664e33cd88c6840b7625b826ecf2555f32d491800900f5a541f772c485f7cda7", size = 381077, upload-time = "2026-02-16T19:10:13.74Z" }, ] [[package]] @@ -51,76 +51,112 @@ wheels = [ [[package]] name = "certifi" -version = "2025.8.3" +version = "2026.4.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, ] [[package]] name = "charset-normalizer" -version = "3.4.3" +version = "3.4.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, - { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, - { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" }, - { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" }, - { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" }, - { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" }, - { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" }, - { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" }, - { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" }, - { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, - { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, - { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, - { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, - { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, - { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, - { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, - { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, - { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, - { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, - { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, - { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, - { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, - { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, - { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, - { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, - { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, - { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, - { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, - { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, - { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, - { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, - { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, - { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, - { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, - { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, - { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, - { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, ] [[package]] name = "click" -version = "8.2.1" +version = "8.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, ] [[package]] @@ -134,77 +170,101 @@ wheels = [ [[package]] name = "coverage" -version = "7.10.5" +version = "7.13.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/61/83/153f54356c7c200013a752ce1ed5448573dca546ce125801afca9e1ac1a4/coverage-7.10.5.tar.gz", hash = "sha256:f2e57716a78bc3ae80b2207be0709a3b2b63b9f2dcf9740ee6ac03588a2015b6", size = 821662, upload-time = "2025-08-23T14:42:44.78Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/f2/336d34d2fc1291ca7c18eeb46f64985e6cef5a1a7ef6d9c23720c6527289/coverage-7.10.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c177e6ffe2ebc7c410785307758ee21258aa8e8092b44d09a2da767834f075f2", size = 216890, upload-time = "2025-08-23T14:40:43.627Z" }, - { url = "https://files.pythonhosted.org/packages/39/ea/92448b07cc1cf2b429d0ce635f59cf0c626a5d8de21358f11e92174ff2a6/coverage-7.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:14d6071c51ad0f703d6440827eaa46386169b5fdced42631d5a5ac419616046f", size = 217287, upload-time = "2025-08-23T14:40:45.214Z" }, - { url = "https://files.pythonhosted.org/packages/96/ba/ad5b36537c5179c808d0ecdf6e4aa7630b311b3c12747ad624dcd43a9b6b/coverage-7.10.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:61f78c7c3bc272a410c5ae3fde7792b4ffb4acc03d35a7df73ca8978826bb7ab", size = 247683, upload-time = "2025-08-23T14:40:46.791Z" }, - { url = "https://files.pythonhosted.org/packages/28/e5/fe3bbc8d097029d284b5fb305b38bb3404895da48495f05bff025df62770/coverage-7.10.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f39071caa126f69d63f99b324fb08c7b1da2ec28cbb1fe7b5b1799926492f65c", size = 249614, upload-time = "2025-08-23T14:40:48.082Z" }, - { url = "https://files.pythonhosted.org/packages/69/9c/a1c89a8c8712799efccb32cd0a1ee88e452f0c13a006b65bb2271f1ac767/coverage-7.10.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343a023193f04d46edc46b2616cdbee68c94dd10208ecd3adc56fcc54ef2baa1", size = 251719, upload-time = "2025-08-23T14:40:49.349Z" }, - { url = "https://files.pythonhosted.org/packages/e9/be/5576b5625865aa95b5633315f8f4142b003a70c3d96e76f04487c3b5cc95/coverage-7.10.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:585ffe93ae5894d1ebdee69fc0b0d4b7c75d8007983692fb300ac98eed146f78", size = 249411, upload-time = "2025-08-23T14:40:50.624Z" }, - { url = "https://files.pythonhosted.org/packages/94/0a/e39a113d4209da0dbbc9385608cdb1b0726a4d25f78672dc51c97cfea80f/coverage-7.10.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0ef4e66f006ed181df29b59921bd8fc7ed7cd6a9289295cd8b2824b49b570df", size = 247466, upload-time = "2025-08-23T14:40:52.362Z" }, - { url = "https://files.pythonhosted.org/packages/40/cb/aebb2d8c9e3533ee340bea19b71c5b76605a0268aa49808e26fe96ec0a07/coverage-7.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eb7b0bbf7cc1d0453b843eca7b5fa017874735bef9bfdfa4121373d2cc885ed6", size = 248104, upload-time = "2025-08-23T14:40:54.064Z" }, - { url = "https://files.pythonhosted.org/packages/08/e6/26570d6ccce8ff5de912cbfd268e7f475f00597cb58da9991fa919c5e539/coverage-7.10.5-cp311-cp311-win32.whl", hash = "sha256:1d043a8a06987cc0c98516e57c4d3fc2c1591364831e9deb59c9e1b4937e8caf", size = 219327, upload-time = "2025-08-23T14:40:55.424Z" }, - { url = "https://files.pythonhosted.org/packages/79/79/5f48525e366e518b36e66167e3b6e5db6fd54f63982500c6a5abb9d3dfbd/coverage-7.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:fefafcca09c3ac56372ef64a40f5fe17c5592fab906e0fdffd09543f3012ba50", size = 220213, upload-time = "2025-08-23T14:40:56.724Z" }, - { url = "https://files.pythonhosted.org/packages/40/3c/9058128b7b0bf333130c320b1eb1ae485623014a21ee196d68f7737f8610/coverage-7.10.5-cp311-cp311-win_arm64.whl", hash = "sha256:7e78b767da8b5fc5b2faa69bb001edafcd6f3995b42a331c53ef9572c55ceb82", size = 218893, upload-time = "2025-08-23T14:40:58.011Z" }, - { url = "https://files.pythonhosted.org/packages/27/8e/40d75c7128f871ea0fd829d3e7e4a14460cad7c3826e3b472e6471ad05bd/coverage-7.10.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c2d05c7e73c60a4cecc7d9b60dbfd603b4ebc0adafaef371445b47d0f805c8a9", size = 217077, upload-time = "2025-08-23T14:40:59.329Z" }, - { url = "https://files.pythonhosted.org/packages/18/a8/f333f4cf3fb5477a7f727b4d603a2eb5c3c5611c7fe01329c2e13b23b678/coverage-7.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:32ddaa3b2c509778ed5373b177eb2bf5662405493baeff52278a0b4f9415188b", size = 217310, upload-time = "2025-08-23T14:41:00.628Z" }, - { url = "https://files.pythonhosted.org/packages/ec/2c/fbecd8381e0a07d1547922be819b4543a901402f63930313a519b937c668/coverage-7.10.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dd382410039fe062097aa0292ab6335a3f1e7af7bba2ef8d27dcda484918f20c", size = 248802, upload-time = "2025-08-23T14:41:02.012Z" }, - { url = "https://files.pythonhosted.org/packages/3f/bc/1011da599b414fb6c9c0f34086736126f9ff71f841755786a6b87601b088/coverage-7.10.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7fa22800f3908df31cea6fb230f20ac49e343515d968cc3a42b30d5c3ebf9b5a", size = 251550, upload-time = "2025-08-23T14:41:03.438Z" }, - { url = "https://files.pythonhosted.org/packages/4c/6f/b5c03c0c721c067d21bc697accc3642f3cef9f087dac429c918c37a37437/coverage-7.10.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f366a57ac81f5e12797136552f5b7502fa053c861a009b91b80ed51f2ce651c6", size = 252684, upload-time = "2025-08-23T14:41:04.85Z" }, - { url = "https://files.pythonhosted.org/packages/f9/50/d474bc300ebcb6a38a1047d5c465a227605d6473e49b4e0d793102312bc5/coverage-7.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f1dc8f1980a272ad4a6c84cba7981792344dad33bf5869361576b7aef42733a", size = 250602, upload-time = "2025-08-23T14:41:06.719Z" }, - { url = "https://files.pythonhosted.org/packages/4a/2d/548c8e04249cbba3aba6bd799efdd11eee3941b70253733f5d355d689559/coverage-7.10.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2285c04ee8676f7938b02b4936d9b9b672064daab3187c20f73a55f3d70e6b4a", size = 248724, upload-time = "2025-08-23T14:41:08.429Z" }, - { url = "https://files.pythonhosted.org/packages/e2/96/a7c3c0562266ac39dcad271d0eec8fc20ab576e3e2f64130a845ad2a557b/coverage-7.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2492e4dd9daab63f5f56286f8a04c51323d237631eb98505d87e4c4ff19ec34", size = 250158, upload-time = "2025-08-23T14:41:09.749Z" }, - { url = "https://files.pythonhosted.org/packages/f3/75/74d4be58c70c42ef0b352d597b022baf12dbe2b43e7cb1525f56a0fb1d4b/coverage-7.10.5-cp312-cp312-win32.whl", hash = "sha256:38a9109c4ee8135d5df5505384fc2f20287a47ccbe0b3f04c53c9a1989c2bbaf", size = 219493, upload-time = "2025-08-23T14:41:11.095Z" }, - { url = "https://files.pythonhosted.org/packages/4f/08/364e6012d1d4d09d1e27437382967efed971d7613f94bca9add25f0c1f2b/coverage-7.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:6b87f1ad60b30bc3c43c66afa7db6b22a3109902e28c5094957626a0143a001f", size = 220302, upload-time = "2025-08-23T14:41:12.449Z" }, - { url = "https://files.pythonhosted.org/packages/db/d5/7c8a365e1f7355c58af4fe5faf3f90cc8e587590f5854808d17ccb4e7077/coverage-7.10.5-cp312-cp312-win_arm64.whl", hash = "sha256:672a6c1da5aea6c629819a0e1461e89d244f78d7b60c424ecf4f1f2556c041d8", size = 218936, upload-time = "2025-08-23T14:41:13.872Z" }, - { url = "https://files.pythonhosted.org/packages/9f/08/4166ecfb60ba011444f38a5a6107814b80c34c717bc7a23be0d22e92ca09/coverage-7.10.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ef3b83594d933020f54cf65ea1f4405d1f4e41a009c46df629dd964fcb6e907c", size = 217106, upload-time = "2025-08-23T14:41:15.268Z" }, - { url = "https://files.pythonhosted.org/packages/25/d7/b71022408adbf040a680b8c64bf6ead3be37b553e5844f7465643979f7ca/coverage-7.10.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b96bfdf7c0ea9faebce088a3ecb2382819da4fbc05c7b80040dbc428df6af44", size = 217353, upload-time = "2025-08-23T14:41:16.656Z" }, - { url = "https://files.pythonhosted.org/packages/74/68/21e0d254dbf8972bb8dd95e3fe7038f4be037ff04ba47d6d1b12b37510ba/coverage-7.10.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:63df1fdaffa42d914d5c4d293e838937638bf75c794cf20bee12978fc8c4e3bc", size = 248350, upload-time = "2025-08-23T14:41:18.128Z" }, - { url = "https://files.pythonhosted.org/packages/90/65/28752c3a896566ec93e0219fc4f47ff71bd2b745f51554c93e8dcb659796/coverage-7.10.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8002dc6a049aac0e81ecec97abfb08c01ef0c1fbf962d0c98da3950ace89b869", size = 250955, upload-time = "2025-08-23T14:41:19.577Z" }, - { url = "https://files.pythonhosted.org/packages/a5/eb/ca6b7967f57f6fef31da8749ea20417790bb6723593c8cd98a987be20423/coverage-7.10.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63d4bb2966d6f5f705a6b0c6784c8969c468dbc4bcf9d9ded8bff1c7e092451f", size = 252230, upload-time = "2025-08-23T14:41:20.959Z" }, - { url = "https://files.pythonhosted.org/packages/bc/29/17a411b2a2a18f8b8c952aa01c00f9284a1fbc677c68a0003b772ea89104/coverage-7.10.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1f672efc0731a6846b157389b6e6d5d5e9e59d1d1a23a5c66a99fd58339914d5", size = 250387, upload-time = "2025-08-23T14:41:22.644Z" }, - { url = "https://files.pythonhosted.org/packages/c7/89/97a9e271188c2fbb3db82235c33980bcbc733da7da6065afbaa1d685a169/coverage-7.10.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3f39cef43d08049e8afc1fde4a5da8510fc6be843f8dea350ee46e2a26b2f54c", size = 248280, upload-time = "2025-08-23T14:41:24.061Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c6/0ad7d0137257553eb4706b4ad6180bec0a1b6a648b092c5bbda48d0e5b2c/coverage-7.10.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2968647e3ed5a6c019a419264386b013979ff1fb67dd11f5c9886c43d6a31fc2", size = 249894, upload-time = "2025-08-23T14:41:26.165Z" }, - { url = "https://files.pythonhosted.org/packages/84/56/fb3aba936addb4c9e5ea14f5979393f1c2466b4c89d10591fd05f2d6b2aa/coverage-7.10.5-cp313-cp313-win32.whl", hash = "sha256:0d511dda38595b2b6934c2b730a1fd57a3635c6aa2a04cb74714cdfdd53846f4", size = 219536, upload-time = "2025-08-23T14:41:27.694Z" }, - { url = "https://files.pythonhosted.org/packages/fc/54/baacb8f2f74431e3b175a9a2881feaa8feb6e2f187a0e7e3046f3c7742b2/coverage-7.10.5-cp313-cp313-win_amd64.whl", hash = "sha256:9a86281794a393513cf117177fd39c796b3f8e3759bb2764259a2abba5cce54b", size = 220330, upload-time = "2025-08-23T14:41:29.081Z" }, - { url = "https://files.pythonhosted.org/packages/64/8a/82a3788f8e31dee51d350835b23d480548ea8621f3effd7c3ba3f7e5c006/coverage-7.10.5-cp313-cp313-win_arm64.whl", hash = "sha256:cebd8e906eb98bb09c10d1feed16096700b1198d482267f8bf0474e63a7b8d84", size = 218961, upload-time = "2025-08-23T14:41:30.511Z" }, - { url = "https://files.pythonhosted.org/packages/d8/a1/590154e6eae07beee3b111cc1f907c30da6fc8ce0a83ef756c72f3c7c748/coverage-7.10.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0520dff502da5e09d0d20781df74d8189ab334a1e40d5bafe2efaa4158e2d9e7", size = 217819, upload-time = "2025-08-23T14:41:31.962Z" }, - { url = "https://files.pythonhosted.org/packages/0d/ff/436ffa3cfc7741f0973c5c89405307fe39b78dcf201565b934e6616fc4ad/coverage-7.10.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d9cd64aca68f503ed3f1f18c7c9174cbb797baba02ca8ab5112f9d1c0328cd4b", size = 218040, upload-time = "2025-08-23T14:41:33.472Z" }, - { url = "https://files.pythonhosted.org/packages/a0/ca/5787fb3d7820e66273913affe8209c534ca11241eb34ee8c4fd2aaa9dd87/coverage-7.10.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0913dd1613a33b13c4f84aa6e3f4198c1a21ee28ccb4f674985c1f22109f0aae", size = 259374, upload-time = "2025-08-23T14:41:34.914Z" }, - { url = "https://files.pythonhosted.org/packages/b5/89/21af956843896adc2e64fc075eae3c1cadb97ee0a6960733e65e696f32dd/coverage-7.10.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1b7181c0feeb06ed8a02da02792f42f829a7b29990fef52eff257fef0885d760", size = 261551, upload-time = "2025-08-23T14:41:36.333Z" }, - { url = "https://files.pythonhosted.org/packages/e1/96/390a69244ab837e0ac137989277879a084c786cf036c3c4a3b9637d43a89/coverage-7.10.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36d42b7396b605f774d4372dd9c49bed71cbabce4ae1ccd074d155709dd8f235", size = 263776, upload-time = "2025-08-23T14:41:38.25Z" }, - { url = "https://files.pythonhosted.org/packages/00/32/cfd6ae1da0a521723349f3129b2455832fc27d3f8882c07e5b6fefdd0da2/coverage-7.10.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b4fdc777e05c4940b297bf47bf7eedd56a39a61dc23ba798e4b830d585486ca5", size = 261326, upload-time = "2025-08-23T14:41:40.343Z" }, - { url = "https://files.pythonhosted.org/packages/4c/c4/bf8d459fb4ce2201e9243ce6c015936ad283a668774430a3755f467b39d1/coverage-7.10.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:42144e8e346de44a6f1dbd0a56575dd8ab8dfa7e9007da02ea5b1c30ab33a7db", size = 259090, upload-time = "2025-08-23T14:41:42.106Z" }, - { url = "https://files.pythonhosted.org/packages/f4/5d/a234f7409896468e5539d42234016045e4015e857488b0b5b5f3f3fa5f2b/coverage-7.10.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:66c644cbd7aed8fe266d5917e2c9f65458a51cfe5eeff9c05f15b335f697066e", size = 260217, upload-time = "2025-08-23T14:41:43.591Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ad/87560f036099f46c2ddd235be6476dd5c1d6be6bb57569a9348d43eeecea/coverage-7.10.5-cp313-cp313t-win32.whl", hash = "sha256:2d1b73023854068c44b0c554578a4e1ef1b050ed07cf8b431549e624a29a66ee", size = 220194, upload-time = "2025-08-23T14:41:45.051Z" }, - { url = "https://files.pythonhosted.org/packages/36/a8/04a482594fdd83dc677d4a6c7e2d62135fff5a1573059806b8383fad9071/coverage-7.10.5-cp313-cp313t-win_amd64.whl", hash = "sha256:54a1532c8a642d8cc0bd5a9a51f5a9dcc440294fd06e9dda55e743c5ec1a8f14", size = 221258, upload-time = "2025-08-23T14:41:46.44Z" }, - { url = "https://files.pythonhosted.org/packages/eb/ad/7da28594ab66fe2bc720f1bc9b131e62e9b4c6e39f044d9a48d18429cc21/coverage-7.10.5-cp313-cp313t-win_arm64.whl", hash = "sha256:74d5b63fe3f5f5d372253a4ef92492c11a4305f3550631beaa432fc9df16fcff", size = 219521, upload-time = "2025-08-23T14:41:47.882Z" }, - { url = "https://files.pythonhosted.org/packages/d3/7f/c8b6e4e664b8a95254c35a6c8dd0bf4db201ec681c169aae2f1256e05c85/coverage-7.10.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:68c5e0bc5f44f68053369fa0d94459c84548a77660a5f2561c5e5f1e3bed7031", size = 217090, upload-time = "2025-08-23T14:41:49.327Z" }, - { url = "https://files.pythonhosted.org/packages/44/74/3ee14ede30a6e10a94a104d1d0522d5fb909a7c7cac2643d2a79891ff3b9/coverage-7.10.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cf33134ffae93865e32e1e37df043bef15a5e857d8caebc0099d225c579b0fa3", size = 217365, upload-time = "2025-08-23T14:41:50.796Z" }, - { url = "https://files.pythonhosted.org/packages/41/5f/06ac21bf87dfb7620d1f870dfa3c2cae1186ccbcdc50b8b36e27a0d52f50/coverage-7.10.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ad8fa9d5193bafcf668231294241302b5e683a0518bf1e33a9a0dfb142ec3031", size = 248413, upload-time = "2025-08-23T14:41:52.5Z" }, - { url = "https://files.pythonhosted.org/packages/21/bc/cc5bed6e985d3a14228539631573f3863be6a2587381e8bc5fdf786377a1/coverage-7.10.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:146fa1531973d38ab4b689bc764592fe6c2f913e7e80a39e7eeafd11f0ef6db2", size = 250943, upload-time = "2025-08-23T14:41:53.922Z" }, - { url = "https://files.pythonhosted.org/packages/8d/43/6a9fc323c2c75cd80b18d58db4a25dc8487f86dd9070f9592e43e3967363/coverage-7.10.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6013a37b8a4854c478d3219ee8bc2392dea51602dd0803a12d6f6182a0061762", size = 252301, upload-time = "2025-08-23T14:41:56.528Z" }, - { url = "https://files.pythonhosted.org/packages/69/7c/3e791b8845f4cd515275743e3775adb86273576596dc9f02dca37357b4f2/coverage-7.10.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:eb90fe20db9c3d930fa2ad7a308207ab5b86bf6a76f54ab6a40be4012d88fcae", size = 250302, upload-time = "2025-08-23T14:41:58.171Z" }, - { url = "https://files.pythonhosted.org/packages/5c/bc/5099c1e1cb0c9ac6491b281babea6ebbf999d949bf4aa8cdf4f2b53505e8/coverage-7.10.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:384b34482272e960c438703cafe63316dfbea124ac62006a455c8410bf2a2262", size = 248237, upload-time = "2025-08-23T14:41:59.703Z" }, - { url = "https://files.pythonhosted.org/packages/7e/51/d346eb750a0b2f1e77f391498b753ea906fde69cc11e4b38dca28c10c88c/coverage-7.10.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:467dc74bd0a1a7de2bedf8deaf6811f43602cb532bd34d81ffd6038d6d8abe99", size = 249726, upload-time = "2025-08-23T14:42:01.343Z" }, - { url = "https://files.pythonhosted.org/packages/a3/85/eebcaa0edafe427e93286b94f56ea7e1280f2c49da0a776a6f37e04481f9/coverage-7.10.5-cp314-cp314-win32.whl", hash = "sha256:556d23d4e6393ca898b2e63a5bca91e9ac2d5fb13299ec286cd69a09a7187fde", size = 219825, upload-time = "2025-08-23T14:42:03.263Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f7/6d43e037820742603f1e855feb23463979bf40bd27d0cde1f761dcc66a3e/coverage-7.10.5-cp314-cp314-win_amd64.whl", hash = "sha256:f4446a9547681533c8fa3e3c6cf62121eeee616e6a92bd9201c6edd91beffe13", size = 220618, upload-time = "2025-08-23T14:42:05.037Z" }, - { url = "https://files.pythonhosted.org/packages/4a/b0/ed9432e41424c51509d1da603b0393404b828906236fb87e2c8482a93468/coverage-7.10.5-cp314-cp314-win_arm64.whl", hash = "sha256:5e78bd9cf65da4c303bf663de0d73bf69f81e878bf72a94e9af67137c69b9fe9", size = 219199, upload-time = "2025-08-23T14:42:06.662Z" }, - { url = "https://files.pythonhosted.org/packages/2f/54/5a7ecfa77910f22b659c820f67c16fc1e149ed132ad7117f0364679a8fa9/coverage-7.10.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5661bf987d91ec756a47c7e5df4fbcb949f39e32f9334ccd3f43233bbb65e508", size = 217833, upload-time = "2025-08-23T14:42:08.262Z" }, - { url = "https://files.pythonhosted.org/packages/4e/0e/25672d917cc57857d40edf38f0b867fb9627115294e4f92c8fcbbc18598d/coverage-7.10.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a46473129244db42a720439a26984f8c6f834762fc4573616c1f37f13994b357", size = 218048, upload-time = "2025-08-23T14:42:10.247Z" }, - { url = "https://files.pythonhosted.org/packages/cb/7c/0b2b4f1c6f71885d4d4b2b8608dcfc79057adb7da4143eb17d6260389e42/coverage-7.10.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1f64b8d3415d60f24b058b58d859e9512624bdfa57a2d1f8aff93c1ec45c429b", size = 259549, upload-time = "2025-08-23T14:42:11.811Z" }, - { url = "https://files.pythonhosted.org/packages/94/73/abb8dab1609abec7308d83c6aec547944070526578ee6c833d2da9a0ad42/coverage-7.10.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:44d43de99a9d90b20e0163f9770542357f58860a26e24dc1d924643bd6aa7cb4", size = 261715, upload-time = "2025-08-23T14:42:13.505Z" }, - { url = "https://files.pythonhosted.org/packages/0b/d1/abf31de21ec92731445606b8d5e6fa5144653c2788758fcf1f47adb7159a/coverage-7.10.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a931a87e5ddb6b6404e65443b742cb1c14959622777f2a4efd81fba84f5d91ba", size = 263969, upload-time = "2025-08-23T14:42:15.422Z" }, - { url = "https://files.pythonhosted.org/packages/9c/b3/ef274927f4ebede96056173b620db649cc9cb746c61ffc467946b9d0bc67/coverage-7.10.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9559b906a100029274448f4c8b8b0a127daa4dade5661dfd821b8c188058842", size = 261408, upload-time = "2025-08-23T14:42:16.971Z" }, - { url = "https://files.pythonhosted.org/packages/20/fc/83ca2812be616d69b4cdd4e0c62a7bc526d56875e68fd0f79d47c7923584/coverage-7.10.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b08801e25e3b4526ef9ced1aa29344131a8f5213c60c03c18fe4c6170ffa2874", size = 259168, upload-time = "2025-08-23T14:42:18.512Z" }, - { url = "https://files.pythonhosted.org/packages/fc/4f/e0779e5716f72d5c9962e709d09815d02b3b54724e38567308304c3fc9df/coverage-7.10.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ed9749bb8eda35f8b636fb7632f1c62f735a236a5d4edadd8bbcc5ea0542e732", size = 260317, upload-time = "2025-08-23T14:42:20.005Z" }, - { url = "https://files.pythonhosted.org/packages/2b/fe/4247e732f2234bb5eb9984a0888a70980d681f03cbf433ba7b48f08ca5d5/coverage-7.10.5-cp314-cp314t-win32.whl", hash = "sha256:609b60d123fc2cc63ccee6d17e4676699075db72d14ac3c107cc4976d516f2df", size = 220600, upload-time = "2025-08-23T14:42:22.027Z" }, - { url = "https://files.pythonhosted.org/packages/a7/a0/f294cff6d1034b87839987e5b6ac7385bec599c44d08e0857ac7f164ad0c/coverage-7.10.5-cp314-cp314t-win_amd64.whl", hash = "sha256:0666cf3d2c1626b5a3463fd5b05f5e21f99e6aec40a3192eee4d07a15970b07f", size = 221714, upload-time = "2025-08-23T14:42:23.616Z" }, - { url = "https://files.pythonhosted.org/packages/23/18/fa1afdc60b5528d17416df440bcbd8fd12da12bfea9da5b6ae0f7a37d0f7/coverage-7.10.5-cp314-cp314t-win_arm64.whl", hash = "sha256:bc85eb2d35e760120540afddd3044a5bf69118a91a296a8b3940dfc4fdcfe1e2", size = 219735, upload-time = "2025-08-23T14:42:25.156Z" }, - { url = "https://files.pythonhosted.org/packages/08/b6/fff6609354deba9aeec466e4bcaeb9d1ed3e5d60b14b57df2a36fb2273f2/coverage-7.10.5-py3-none-any.whl", hash = "sha256:0be24d35e4db1d23d0db5c0f6a74a962e2ec83c426b5cac09f4234aadef38e4a", size = 208736, upload-time = "2025-08-23T14:42:43.145Z" }, + { url = "https://files.pythonhosted.org/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d", size = 219381, upload-time = "2026-03-17T10:30:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587", size = 219880, upload-time = "2026-03-17T10:30:16.231Z" }, + { url = "https://files.pythonhosted.org/packages/55/2f/e0e5b237bffdb5d6c530ce87cc1d413a5b7d7dfd60fb067ad6d254c35c76/coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642", size = 250303, upload-time = "2026-03-17T10:30:17.748Z" }, + { url = "https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b", size = 252218, upload-time = "2026-03-17T10:30:19.804Z" }, + { url = "https://files.pythonhosted.org/packages/da/69/2f47bb6fa1b8d1e3e5d0c4be8ccb4313c63d742476a619418f85740d597b/coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686", size = 254326, upload-time = "2026-03-17T10:30:21.321Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d0/79db81da58965bd29dabc8f4ad2a2af70611a57cba9d1ec006f072f30a54/coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743", size = 256267, upload-time = "2026-03-17T10:30:23.094Z" }, + { url = "https://files.pythonhosted.org/packages/e5/32/d0d7cc8168f91ddab44c0ce4806b969df5f5fdfdbb568eaca2dbc2a04936/coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75", size = 250430, upload-time = "2026-03-17T10:30:25.311Z" }, + { url = "https://files.pythonhosted.org/packages/4d/06/a055311d891ddbe231cd69fdd20ea4be6e3603ffebddf8704b8ca8e10a3c/coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209", size = 252017, upload-time = "2026-03-17T10:30:27.284Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f6/d0fd2d21e29a657b5f77a2fe7082e1568158340dceb941954f776dce1b7b/coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a", size = 250080, upload-time = "2026-03-17T10:30:29.481Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ab/0d7fb2efc2e9a5eb7ddcc6e722f834a69b454b7e6e5888c3a8567ecffb31/coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e", size = 253843, upload-time = "2026-03-17T10:30:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/ba/6f/7467b917bbf5408610178f62a49c0ed4377bb16c1657f689cc61470da8ce/coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd", size = 249802, upload-time = "2026-03-17T10:30:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/75/2c/1172fb689df92135f5bfbbd69fc83017a76d24ea2e2f3a1154007e2fb9f8/coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8", size = 250707, upload-time = "2026-03-17T10:30:35.2Z" }, + { url = "https://files.pythonhosted.org/packages/67/21/9ac389377380a07884e3b48ba7a620fcd9dbfaf1d40565facdc6b36ec9ef/coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf", size = 221880, upload-time = "2026-03-17T10:30:36.775Z" }, + { url = "https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9", size = 222816, upload-time = "2026-03-17T10:30:38.891Z" }, + { url = "https://files.pythonhosted.org/packages/12/a6/1d3f6155fb0010ca68eba7fe48ca6c9da7385058b77a95848710ecf189b1/coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028", size = 221483, upload-time = "2026-03-17T10:30:40.463Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, + { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, + { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, + { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, + { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, + { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, + { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, + { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, + { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, ] [package.optional-dependencies] @@ -214,14 +274,14 @@ toml = [ [[package]] name = "fieldz" -version = "0.1.2" +version = "0.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/90/62/698c5cc2e7d4c8c89e63033e2e9d3c74902a1bf28782712eacb0653097ce/fieldz-0.1.2.tar.gz", hash = "sha256:0448ed5dacb13eaa49da0db786e87fae298fbd2652d26c510e5d7aea6b6bebf4", size = 17277, upload-time = "2025-06-30T18:06:40.881Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/46/7a8d1db959eabd5d3e52bd3c000a8b132184b7651cbd5481c4cbf31ff65d/fieldz-0.2.0.tar.gz", hash = "sha256:e11215188cad5c5371113d2b7707155960efd0d48500b6bf675648bc3f6fc8d6", size = 18222, upload-time = "2026-02-24T19:26:02.251Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/8c/8958392cade27a272daf45d09a08473073dedeccad94b097dfeb898d969f/fieldz-0.1.2-py3-none-any.whl", hash = "sha256:e25884d2821a2d5638ef8d4d8bce5d1039359cfcb46d0f93df8cb1f7c2eb3a2e", size = 17878, upload-time = "2025-06-30T18:06:39.322Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9d/9034acaf3d80e85deb3d49876cb734479327b9cf89a918815ef67cb6b36c/fieldz-0.2.0-py3-none-any.whl", hash = "sha256:43b5be702816df39f55d08ae392eaabe58d3c69d2e4b61a853252cb2da41931f", size = 17946, upload-time = "2026-02-24T19:26:00.931Z" }, ] [[package]] @@ -250,39 +310,36 @@ wheels = [ [[package]] name = "gitpython" -version = "3.1.45" +version = "3.1.47" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076, upload-time = "2025-07-24T03:45:54.871Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c1/bd/50db468e9b1310529a19fce651b3b0e753b5c07954d486cba31bbee9a5d5/gitpython-3.1.47.tar.gz", hash = "sha256:dba27f922bd2b42cb54c87a8ab3cb6beb6bf07f3d564e21ac848913a05a8a3cd", size = 216978, upload-time = "2026-04-22T02:44:44.059Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168, upload-time = "2025-07-24T03:45:52.517Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c5/a1bc0996af85757903cf2bf444a7824e68e0035ce63fb41d6f76f9def68b/gitpython-3.1.47-py3-none-any.whl", hash = "sha256:489f590edfd6d20571b2c0e72c6a6ac6915ee8b8cd04572330e3842207a78905", size = 209547, upload-time = "2026-04-22T02:44:41.271Z" }, ] [[package]] -name = "griffe" -version = "1.13.0" +name = "griffe-fieldz" +version = "0.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama" }, + { name = "fieldz" }, + { name = "griffelib" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/b5/23b91f22b7b3a7f8f62223f6664946271c0f5cb4179605a3e6bbae863920/griffe-1.13.0.tar.gz", hash = "sha256:246ea436a5e78f7fbf5f24ca8a727bb4d2a4b442a2959052eea3d0bfe9a076e0", size = 412759, upload-time = "2025-08-26T13:27:11.422Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/7f/b57a27807536590f0cabdd2e6c0faac78c7d6a740e096684c51c503b7003/griffe_fieldz-0.5.0.tar.gz", hash = "sha256:dcf237a0def9f5c15e15aa5cf07b3d0a4edf4f7d6a2d713a45afe29c8af9bc8e", size = 14250, upload-time = "2026-02-24T16:22:23.374Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/8c/b7cfdd8dfe48f6b09f7353323732e1a290c388bd14f216947928dc85f904/griffe-1.13.0-py3-none-any.whl", hash = "sha256:470fde5b735625ac0a36296cd194617f039e9e83e301fcbd493e2b58382d0559", size = 139365, upload-time = "2025-08-26T13:27:09.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/66/9daa3118d9bf4443778d6ee31b024c79766eeb116b275eca71597adae43a/griffe_fieldz-0.5.0-py3-none-any.whl", hash = "sha256:d363e56a7468019a7184ea3995277a1f20507bb9a345408c1f7014ae06878ebf", size = 9629, upload-time = "2026-02-24T16:22:22.094Z" }, ] [[package]] -name = "griffe-fieldz" -version = "0.3.0" +name = "griffelib" +version = "2.0.2" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "fieldz" }, - { name = "griffe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c5/6a/94754bf39fd63ba424c667b2abf0ade78e3878e223591d1fb9c3e8a77bce/griffe_fieldz-0.3.0.tar.gz", hash = "sha256:42e7707dac51d38e26fb7f3f7f51429da9b47e98060bfeb81a4287456d5b8a89", size = 10149, upload-time = "2025-07-30T21:43:10.042Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/33/cc527c11132a6274724a04938d50e1ff2b54a5f5943cd0480427571e1adb/griffe_fieldz-0.3.0-py3-none-any.whl", hash = "sha256:52e02fdcbdf6dea3c8c95756d1e0b30861569f871d19437fda702776fde4e64d", size = 6577, upload-time = "2025-07-30T21:43:09.073Z" }, + { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" }, ] [[package]] @@ -319,20 +376,20 @@ requires-dist = [ [[package]] name = "idna" -version = "3.10" +version = "3.13" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, + { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, ] [[package]] name = "iniconfig" -version = "2.1.0" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] @@ -349,59 +406,85 @@ wheels = [ [[package]] name = "markdown" -version = "3.8.2" +version = "3.10.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071, upload-time = "2025-06-19T17:12:44.483Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827, upload-time = "2025-06-19T17:12:42.994Z" }, + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, ] [[package]] name = "markupsafe" -version = "3.0.2" +version = "3.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, - { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, - { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, - { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, - { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, - { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, - { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, - { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, - { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, - { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, - { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, - { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, - { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, - { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, - { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, - { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, - { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, - { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, - { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, - { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, - { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, - { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, - { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, - { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, - { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, - { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, - { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, - { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, - { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, - { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, - { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, - { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, - { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, - { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, - { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, - { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] [[package]] @@ -439,43 +522,43 @@ wheels = [ [[package]] name = "mkdocs-autorefs" -version = "1.4.3" +version = "1.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "markupsafe" }, { name = "mkdocs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/51/fa/9124cd63d822e2bcbea1450ae68cdc3faf3655c69b455f3a7ed36ce6c628/mkdocs_autorefs-1.4.3.tar.gz", hash = "sha256:beee715b254455c4aa93b6ef3c67579c399ca092259cc41b7d9342573ff1fc75", size = 55425, upload-time = "2025-08-26T14:23:17.223Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/c0/f641843de3f612a6b48253f39244165acff36657a91cc903633d456ae1ac/mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197", size = 56588, upload-time = "2026-02-10T15:23:55.105Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/4d/7123b6fa2278000688ebd338e2a06d16870aaf9eceae6ba047ea05f92df1/mkdocs_autorefs-1.4.3-py3-none-any.whl", hash = "sha256:469d85eb3114801d08e9cc55d102b3ba65917a869b893403b8987b601cf55dc9", size = 25034, upload-time = "2025-08-26T14:23:15.906Z" }, + { url = "https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089", size = 25530, upload-time = "2026-02-10T15:23:53.817Z" }, ] [[package]] name = "mkdocs-codeinclude-plugin" -version = "0.2.1" +version = "0.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mkdocs" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1b/b5/f72df157abc7f85e33ffa417464e9dd535ef5fda7654eda41190047a53b6/mkdocs-codeinclude-plugin-0.2.1.tar.gz", hash = "sha256:305387f67a885f0e36ec1cf977324fe1fe50d31301147194b63631d0864601b1", size = 8140, upload-time = "2023-03-01T19:57:06.724Z" } +sdist = { url = "https://files.pythonhosted.org/packages/31/3b/86c32ce13d966f00c7e05fda0ae824bd7a386a49958a4b18566f3896482d/mkdocs_codeinclude_plugin-0.3.1.tar.gz", hash = "sha256:06bbbf0d4ac7eccaec6e0d89ce76d644a197cfed34880f541516e722ded6512a", size = 12988, upload-time = "2026-02-20T19:11:56.767Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/7b/60573ebf2a22b144eeaf3b29db9a6d4d342d68273f716ea2723d1ad723ba/mkdocs_codeinclude_plugin-0.2.1-py3-none-any.whl", hash = "sha256:172a917c9b257fa62850b669336151f85d3cd40312b2b52520cbcceab557ea6c", size = 8093, upload-time = "2023-03-01T19:57:05.207Z" }, + { url = "https://files.pythonhosted.org/packages/77/ec/fd55b2d0b5e98f002a4bb4ce2b0e6f146703b99a93378fc363e8d9d6ef3e/mkdocs_codeinclude_plugin-0.3.1-py3-none-any.whl", hash = "sha256:443f32c9e4412b84ec084bd2b454020c5bf06cb9a958682e08a528e62b45da4d", size = 8610, upload-time = "2026-02-20T19:11:57.694Z" }, ] [[package]] name = "mkdocs-get-deps" -version = "0.2.0" +version = "0.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mergedeep" }, { name = "platformdirs" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, + { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, ] [[package]] @@ -506,25 +589,24 @@ wheels = [ [[package]] name = "mkdocs-include-markdown-plugin" -version = "7.1.6" +version = "7.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mkdocs" }, { name = "wcmatch" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2c/17/988d97ac6849b196f54d45ca9c60ca894880c160a512785f03834704b3d9/mkdocs_include_markdown_plugin-7.1.6.tar.gz", hash = "sha256:a0753cb82704c10a287f1e789fc9848f82b6beb8749814b24b03dd9f67816677", size = 23391, upload-time = "2025-06-13T18:25:51.193Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/2d/bdf1aee3f4f7b34148b0f62298b62f03415160cb2707f09503c99a0a7cd5/mkdocs_include_markdown_plugin-7.2.2.tar.gz", hash = "sha256:f052ccb741eccf498116b826c1d78a2d761c56747372594709441cee0963fbc9", size = 25415, upload-time = "2026-03-29T15:15:14.2Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/a1/6cf1667a05e5f468e1263fcf848772bca8cc9e358cd57ae19a01f92c9f6f/mkdocs_include_markdown_plugin-7.1.6-py3-none-any.whl", hash = "sha256:7975a593514887c18ecb68e11e35c074c5499cfa3e51b18cd16323862e1f7345", size = 27161, upload-time = "2025-06-13T18:25:49.847Z" }, + { url = "https://files.pythonhosted.org/packages/38/a5/f6b2f0aa805dbda52f6265e9aff1450c8643195442facf29d475bdeba15d/mkdocs_include_markdown_plugin-7.2.2-py3-none-any.whl", hash = "sha256:f2ec4487cf32d3e33ca528f9366f20fb9280ded9c8d1630eb2bbda244962dcd1", size = 29528, upload-time = "2026-03-29T15:15:13.079Z" }, ] [[package]] name = "mkdocs-material" -version = "9.6.18" +version = "9.7.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, { name = "backrefs" }, - { name = "click" }, { name = "colorama" }, { name = "jinja2" }, { name = "markdown" }, @@ -535,9 +617,9 @@ dependencies = [ { name = "pymdown-extensions" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e6/46/db0d78add5aac29dfcd0a593bcc6049c86c77ba8a25b3a5b681c190d5e99/mkdocs_material-9.6.18.tar.gz", hash = "sha256:a2eb253bcc8b66f8c6eaf8379c10ed6e9644090c2e2e9d0971c7722dc7211c05", size = 4034856, upload-time = "2025-08-22T08:21:47.575Z" } +sdist = { url = "https://files.pythonhosted.org/packages/45/29/6d2bcf41ae40802c4beda2432396fff97b8456fb496371d1bc7aad6512ec/mkdocs_material-9.7.6.tar.gz", hash = "sha256:00bdde50574f776d328b1862fe65daeaf581ec309bd150f7bff345a098c64a69", size = 4097959, upload-time = "2026-03-19T15:41:58.161Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/0b/545a4f8d4f9057e77f1d99640eb09aaae40c4f9034707f25636caf716ff9/mkdocs_material-9.6.18-py3-none-any.whl", hash = "sha256:dbc1e146a0ecce951a4d84f97b816a54936cdc9e1edd1667fc6868878ac06701", size = 9232642, upload-time = "2025-08-22T08:21:44.52Z" }, + { url = "https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl", hash = "sha256:71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba", size = 9305470, upload-time = "2026-03-19T15:41:55.217Z" }, ] [[package]] @@ -564,7 +646,7 @@ wheels = [ [[package]] name = "mkdocstrings" -version = "0.30.0" +version = "1.0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jinja2" }, @@ -574,23 +656,23 @@ dependencies = [ { name = "mkdocs-autorefs" }, { name = "pymdown-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e2/0a/7e4776217d4802009c8238c75c5345e23014a4706a8414a62c0498858183/mkdocstrings-0.30.0.tar.gz", hash = "sha256:5d8019b9c31ddacd780b6784ffcdd6f21c408f34c0bd1103b5351d609d5b4444", size = 106597, upload-time = "2025-07-22T23:48:45.998Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/5d/f888d4d3eb31359b327bc9b17a212d6ef03fe0b0682fbb3fc2cb849fb12b/mkdocstrings-1.0.4.tar.gz", hash = "sha256:3969a6515b77db65fd097b53c1b7aa4ae840bd71a2ee62a6a3e89503446d7172", size = 100088, upload-time = "2026-04-15T09:16:53.376Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/b4/3c5eac68f31e124a55d255d318c7445840fa1be55e013f507556d6481913/mkdocstrings-0.30.0-py3-none-any.whl", hash = "sha256:ae9e4a0d8c1789697ac776f2e034e2ddd71054ae1cf2c2bb1433ccfd07c226f2", size = 36579, upload-time = "2025-07-22T23:48:44.152Z" }, + { url = "https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl", hash = "sha256:63464b4b29053514f32a1dbbf604e52876d5e638111b0c295ab7ed3cac73ca9b", size = 35560, upload-time = "2026-04-15T09:16:51.436Z" }, ] [[package]] name = "mkdocstrings-python" -version = "1.18.0" +version = "2.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "griffe" }, + { name = "griffelib" }, { name = "mkdocs-autorefs" }, { name = "mkdocstrings" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/13/d4/6327c4e82dda667b0ff83b6f6b6a03e7b81dfd1f28cd5eda50ffe66d546f/mkdocstrings_python-1.18.0.tar.gz", hash = "sha256:0b9924b4034fe9ae43604d78fe8e5107ea2c2391620124fc833043a62e83c744", size = 207601, upload-time = "2025-08-26T14:02:30.839Z" } +sdist = { url = "https://files.pythonhosted.org/packages/29/33/c225eaf898634bdda489a6766fc35d1683c640bffe0e0acd10646b13536d/mkdocstrings_python-2.0.3.tar.gz", hash = "sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8", size = 199083, upload-time = "2026-02-20T10:38:36.368Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/96/7ecc71bb9f01ee20f201b2531960b401159c6730aec90ec76a1b74bc81e1/mkdocstrings_python-1.18.0-py3-none-any.whl", hash = "sha256:f5056d8afe9a9683ad0c59001df1ecd9668b51c19b9a6b4dc0ff02cc9b76265a", size = 138182, upload-time = "2025-08-26T14:02:28.076Z" }, + { url = "https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl", hash = "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12", size = 104779, upload-time = "2026-02-20T10:38:34.517Z" }, ] [[package]] @@ -674,11 +756,11 @@ wheels = [ [[package]] name = "packaging" -version = "25.0" +version = "26.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] [[package]] @@ -752,20 +834,20 @@ wheels = [ [[package]] name = "pathspec" -version = "0.12.1" +version = "1.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] [[package]] name = "platformdirs" -version = "4.4.0" +version = "4.9.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, ] [[package]] @@ -779,11 +861,11 @@ wheels = [ [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] [[package]] @@ -847,15 +929,15 @@ docs = [ [[package]] name = "pymdown-extensions" -version = "10.16.1" +version = "10.21.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/b3/6d2b3f149bc5413b0a29761c2c5832d8ce904a1d7f621e86616d96f505cc/pymdown_extensions-10.16.1.tar.gz", hash = "sha256:aace82bcccba3efc03e25d584e6a22d27a8e17caa3f4dd9f207e49b787aa9a91", size = 853277, upload-time = "2025-07-28T16:19:34.167Z" } +sdist = { url = "https://files.pythonhosted.org/packages/df/08/f1c908c581fd11913da4711ea7ba32c0eee40b0190000996bb863b0c9349/pymdown_extensions-10.21.2.tar.gz", hash = "sha256:c3f55a5b8a1d0edf6699e35dcbea71d978d34ff3fa79f3d807b8a5b3fa90fbdc", size = 853922, upload-time = "2026-03-29T15:01:55.233Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/06/43084e6cbd4b3bc0e80f6be743b2e79fbc6eed8de9ad8c629939fa55d972/pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d", size = 266178, upload-time = "2025-07-28T16:19:31.401Z" }, + { url = "https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl", hash = "sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638", size = 268901, upload-time = "2026-03-29T15:01:53.244Z" }, ] [[package]] @@ -869,7 +951,7 @@ wheels = [ [[package]] name = "pytest" -version = "8.4.1" +version = "9.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -878,23 +960,23 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] [[package]] name = "pytest-cov" -version = "6.2.1" +version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] [[package]] @@ -923,37 +1005,57 @@ wheels = [ [[package]] name = "pyyaml" -version = "6.0.2" +version = "6.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, - { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, - { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, - { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, - { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, - { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, - { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, - { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, - { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, - { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, - { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, - { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, - { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] [[package]] @@ -970,7 +1072,7 @@ wheels = [ [[package]] name = "requests" -version = "2.32.5" +version = "2.33.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -978,35 +1080,34 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, ] [[package]] name = "ruff" -version = "0.12.10" +version = "0.15.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3b/eb/8c073deb376e46ae767f4961390d17545e8535921d2f65101720ed8bd434/ruff-0.12.10.tar.gz", hash = "sha256:189ab65149d11ea69a2d775343adf5f49bb2426fc4780f65ee33b423ad2e47f9", size = 5310076, upload-time = "2025-08-21T18:23:22.595Z" } +sdist = { url = "https://files.pythonhosted.org/packages/99/43/3291f1cc9106f4c63bdce7a8d0df5047fe8422a75b091c16b5e9355e0b11/ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6", size = 4643852, upload-time = "2026-04-24T18:17:14.305Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/e7/560d049d15585d6c201f9eeacd2fd130def3741323e5ccf123786e0e3c95/ruff-0.12.10-py3-none-linux_armv6l.whl", hash = "sha256:8b593cb0fb55cc8692dac7b06deb29afda78c721c7ccfed22db941201b7b8f7b", size = 11935161, upload-time = "2025-08-21T18:22:26.965Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b0/ad2464922a1113c365d12b8f80ed70fcfb39764288ac77c995156080488d/ruff-0.12.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ebb7333a45d56efc7c110a46a69a1b32365d5c5161e7244aaf3aa20ce62399c1", size = 12660884, upload-time = "2025-08-21T18:22:30.925Z" }, - { url = "https://files.pythonhosted.org/packages/d7/f1/97f509b4108d7bae16c48389f54f005b62ce86712120fd8b2d8e88a7cb49/ruff-0.12.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d59e58586829f8e4a9920788f6efba97a13d1fa320b047814e8afede381c6839", size = 11872754, upload-time = "2025-08-21T18:22:34.035Z" }, - { url = "https://files.pythonhosted.org/packages/12/ad/44f606d243f744a75adc432275217296095101f83f966842063d78eee2d3/ruff-0.12.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:822d9677b560f1fdeab69b89d1f444bf5459da4aa04e06e766cf0121771ab844", size = 12092276, upload-time = "2025-08-21T18:22:36.764Z" }, - { url = "https://files.pythonhosted.org/packages/06/1f/ed6c265e199568010197909b25c896d66e4ef2c5e1c3808caf461f6f3579/ruff-0.12.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:37b4a64f4062a50c75019c61c7017ff598cb444984b638511f48539d3a1c98db", size = 11734700, upload-time = "2025-08-21T18:22:39.822Z" }, - { url = "https://files.pythonhosted.org/packages/63/c5/b21cde720f54a1d1db71538c0bc9b73dee4b563a7dd7d2e404914904d7f5/ruff-0.12.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c6f4064c69d2542029b2a61d39920c85240c39837599d7f2e32e80d36401d6e", size = 13468783, upload-time = "2025-08-21T18:22:42.559Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/39369e6ac7f2a1848f22fb0b00b690492f20811a1ac5c1fd1d2798329263/ruff-0.12.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:059e863ea3a9ade41407ad71c1de2badfbe01539117f38f763ba42a1206f7559", size = 14436642, upload-time = "2025-08-21T18:22:45.612Z" }, - { url = "https://files.pythonhosted.org/packages/e3/03/5da8cad4b0d5242a936eb203b58318016db44f5c5d351b07e3f5e211bb89/ruff-0.12.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bef6161e297c68908b7218fa6e0e93e99a286e5ed9653d4be71e687dff101cf", size = 13859107, upload-time = "2025-08-21T18:22:48.886Z" }, - { url = "https://files.pythonhosted.org/packages/19/19/dd7273b69bf7f93a070c9cec9494a94048325ad18fdcf50114f07e6bf417/ruff-0.12.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4f1345fbf8fb0531cd722285b5f15af49b2932742fc96b633e883da8d841896b", size = 12886521, upload-time = "2025-08-21T18:22:51.567Z" }, - { url = "https://files.pythonhosted.org/packages/c0/1d/b4207ec35e7babaee62c462769e77457e26eb853fbdc877af29417033333/ruff-0.12.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f68433c4fbc63efbfa3ba5db31727db229fa4e61000f452c540474b03de52a9", size = 13097528, upload-time = "2025-08-21T18:22:54.609Z" }, - { url = "https://files.pythonhosted.org/packages/ff/00/58f7b873b21114456e880b75176af3490d7a2836033779ca42f50de3b47a/ruff-0.12.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:141ce3d88803c625257b8a6debf4a0473eb6eed9643a6189b68838b43e78165a", size = 13080443, upload-time = "2025-08-21T18:22:57.413Z" }, - { url = "https://files.pythonhosted.org/packages/12/8c/9e6660007fb10189ccb78a02b41691288038e51e4788bf49b0a60f740604/ruff-0.12.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f3fc21178cd44c98142ae7590f42ddcb587b8e09a3b849cbc84edb62ee95de60", size = 11896759, upload-time = "2025-08-21T18:23:00.473Z" }, - { url = "https://files.pythonhosted.org/packages/67/4c/6d092bb99ea9ea6ebda817a0e7ad886f42a58b4501a7e27cd97371d0ba54/ruff-0.12.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7d1a4e0bdfafcd2e3e235ecf50bf0176f74dd37902f241588ae1f6c827a36c56", size = 11701463, upload-time = "2025-08-21T18:23:03.211Z" }, - { url = "https://files.pythonhosted.org/packages/59/80/d982c55e91df981f3ab62559371380616c57ffd0172d96850280c2b04fa8/ruff-0.12.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e67d96827854f50b9e3e8327b031647e7bcc090dbe7bb11101a81a3a2cbf1cc9", size = 12691603, upload-time = "2025-08-21T18:23:06.935Z" }, - { url = "https://files.pythonhosted.org/packages/ad/37/63a9c788bbe0b0850611669ec6b8589838faf2f4f959647f2d3e320383ae/ruff-0.12.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ae479e1a18b439c59138f066ae79cc0f3ee250712a873d00dbafadaad9481e5b", size = 13164356, upload-time = "2025-08-21T18:23:10.225Z" }, - { url = "https://files.pythonhosted.org/packages/47/d4/1aaa7fb201a74181989970ebccd12f88c0fc074777027e2a21de5a90657e/ruff-0.12.10-py3-none-win32.whl", hash = "sha256:9de785e95dc2f09846c5e6e1d3a3d32ecd0b283a979898ad427a9be7be22b266", size = 11896089, upload-time = "2025-08-21T18:23:14.232Z" }, - { url = "https://files.pythonhosted.org/packages/ad/14/2ad38fd4037daab9e023456a4a40ed0154e9971f8d6aed41bdea390aabd9/ruff-0.12.10-py3-none-win_amd64.whl", hash = "sha256:7837eca8787f076f67aba2ca559cefd9c5cbc3a9852fd66186f4201b87c1563e", size = 13004616, upload-time = "2025-08-21T18:23:17.422Z" }, - { url = "https://files.pythonhosted.org/packages/24/3c/21cf283d67af33a8e6ed242396863af195a8a6134ec581524fd22b9811b6/ruff-0.12.10-py3-none-win_arm64.whl", hash = "sha256:cc138cc06ed9d4bfa9d667a65af7172b47840e1a98b02ce7011c391e54635ffc", size = 12074225, upload-time = "2025-08-21T18:23:20.137Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6e/e78ffb61d4686f3d96ba3df2c801161843746dcbcbb17a1e927d4829312b/ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c", size = 10640713, upload-time = "2026-04-24T18:17:22.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/08/a317bc231fb9e7b93e4ef3089501e51922ff88d6936ce5cf870c4fe55419/ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c", size = 11069267, upload-time = "2026-04-24T18:17:30.105Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5", size = 10397182, upload-time = "2026-04-24T18:17:07.177Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/3310fc6d1b5e1fdea22bf3b1b807c7e187b581021b0d7d4514cccdb5fb71/ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002", size = 10758012, upload-time = "2026-04-24T18:16:55.759Z" }, + { url = "https://files.pythonhosted.org/packages/11/c1/a606911aee04c324ddaa883ae418f3569792fd3c4a10c50e0dd0a2311e1e/ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5", size = 10447479, upload-time = "2026-04-24T18:16:51.677Z" }, + { url = "https://files.pythonhosted.org/packages/9d/68/4201e8444f0894f21ab4aeeaee68aa4f10b51613514a20d80bd628d57e88/ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6", size = 11234040, upload-time = "2026-04-24T18:17:16.529Z" }, + { url = "https://files.pythonhosted.org/packages/34/ff/8a6d6cf4ccc23fd67060874e832c18919d1557a0611ebef03fdb01fff11e/ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33", size = 12087377, upload-time = "2026-04-24T18:17:04.944Z" }, + { url = "https://files.pythonhosted.org/packages/85/f6/c669cf73f5152f623d34e69866a46d5e6185816b19fcd5b6dd8a2d299922/ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847", size = 11367784, upload-time = "2026-04-24T18:17:25.409Z" }, + { url = "https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0", size = 11344088, upload-time = "2026-04-24T18:17:12.258Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8d/49afab3645e31e12c590acb6d3b5b69d7aab5b81926dbaf7461f9441f37a/ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339", size = 11271770, upload-time = "2026-04-24T18:17:02.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/06/33f41fe94403e2b755481cdfb9b7ef3e4e0ed031c4581124658d935d52b4/ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5", size = 10719355, upload-time = "2026-04-24T18:17:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/0d/59/18aa4e014debbf559670e4048e39260a85c7fcee84acfd761ac01e7b8d35/ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd", size = 10462758, upload-time = "2026-04-24T18:17:32.347Z" }, + { url = "https://files.pythonhosted.org/packages/25/e7/cc9f16fd0f3b5fddcbd7ec3d6ae30c8f3fde1047f32a4093a98d633c6570/ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b", size = 10953498, upload-time = "2026-04-24T18:17:20.674Z" }, + { url = "https://files.pythonhosted.org/packages/72/7a/a9ba7f98c7a575978698f4230c5e8cc54bbc761af34f560818f933dafa0c/ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e", size = 11447765, upload-time = "2026-04-24T18:17:09.755Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f9/0ae446942c846b8266059ad8a30702a35afae55f5cdc54c5adf8d7afdc27/ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20", size = 10657277, upload-time = "2026-04-24T18:17:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d", size = 11837758, upload-time = "2026-04-24T18:17:00.113Z" }, + { url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" }, ] [[package]] @@ -1020,11 +1121,11 @@ wheels = [ [[package]] name = "smmap" -version = "5.0.2" +version = "5.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, ] [[package]] @@ -1038,41 +1139,56 @@ wheels = [ [[package]] name = "tomli" -version = "2.2.1" +version = "2.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, - { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, - { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, - { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, - { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, - { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, - { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, - { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, - { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, - { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, - { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, - { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, - { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, - { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, - { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, - { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, - { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, - { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, - { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, - { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, - { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, - { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, - { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, - { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, - { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, - { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, - { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, - { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, - { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] [[package]] @@ -1119,11 +1235,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.5.0" +version = "2.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] [[package]] From bc1c0eaa475a3a02536bf7f77aad9e6803763771 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sun, 26 Apr 2026 19:32:31 -0700 Subject: [PATCH 194/267] Avoid accessing private attribute Co-authored-by: Copilot --- src/harp-protocol/harp/protocol/_register.py | 8 ++++---- tests/protocol/test_payload.py | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/harp-protocol/harp/protocol/_register.py b/src/harp-protocol/harp/protocol/_register.py index b4b94b8..a58041b 100644 --- a/src/harp-protocol/harp/protocol/_register.py +++ b/src/harp-protocol/harp/protocol/_register.py @@ -46,7 +46,7 @@ class TimestampSecond(RegisterU32): P = TypeVar("P", bound=PayloadBase[Any]) _R = TypeVar("_R") -_AR = TypeVar("_AR", bound="RegisterBase[Any]") # type: ignore[type-arg] +_AR = TypeVar("_AR", bound="RegisterBase[Any]") class _RegisterMeta(ABCMeta): @@ -72,13 +72,13 @@ class RegisterBase(ABC, Generic[P]): length: ClassVar[int | None] = None @classmethod - def parse(cls, value: HarpMessage | bytes | bytearray | memoryview) -> P: # type: ignore[type-var] + def parse(cls, value: HarpMessage | bytes | bytearray | memoryview) -> P: """Parse the payload from a ``HarpMessage`` or raw bytes.""" buf = value.payload if isinstance(value, HarpMessage) else value return cast(P, cls.payload_class.from_buffer(buf)) @classmethod - def parse_bulk(cls, data: bytes | bytearray | memoryview | list[HarpMessage]) -> P: # type: ignore[type-var] + def parse_bulk(cls, data: bytes | bytearray | memoryview | list[HarpMessage]) -> P: """Parse a batch of payloads from concatenated bytes or a list of messages.""" if isinstance(data, list): data = b"".join(msg.payload for msg in data) @@ -120,7 +120,7 @@ def format( mt = MessageType.Write if message_type is None else message_type if isinstance(value, PayloadBase): # Payload instance — use its backing array bytes directly - raw = value._arr.tobytes() + raw = value.payload.tobytes() elif isinstance(value, np.ndarray) and value.dtype != cls.payload_type.numpy_dtype: # Structured numpy array passed by hand raw = value.tobytes() diff --git a/tests/protocol/test_payload.py b/tests/protocol/test_payload.py index 46ffd32..e16be06 100644 --- a/tests/protocol/test_payload.py +++ b/tests/protocol/test_payload.py @@ -12,11 +12,11 @@ class SimplePayload(PayloadBase): @property def x(self) -> NDArray[np.int16]: - return self._arr["x"] + return self.payload["x"] @property def y(self) -> NDArray[np.uint8]: - return self._arr["y"] + return self.payload["y"] class BitPackedPayload(PayloadBase): @@ -25,8 +25,8 @@ class BitPackedPayload(PayloadBase): def to_dataframe(self) -> pd.DataFrame: return pd.DataFrame( { - "flag_a": (self._arr["packed"] & 0x01).astype(bool), - "flag_b": ((self._arr["packed"] >> 1) & 0x01).astype(bool), + "flag_a": (self.payload["packed"] & 0x01).astype(bool), + "flag_b": ((self.payload["packed"] >> 1) & 0x01).astype(bool), } ) @@ -72,7 +72,7 @@ def test_from_buffer_zero_copy(): p = SimplePayload.from_buffer(data) # np.frombuffer returns a read-only view — writes should raise with pytest.raises((ValueError, TypeError)): - p._arr["x"][0] = 999 + p.payload["x"][0] = 999 def test_payload_property(): From 8e6f8dd7485fa1252001123662c0f50e806ccaa5 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sun, 26 Apr 2026 20:51:53 -0700 Subject: [PATCH 195/267] Make the mess uniform for now Co-authored-by: Copilot --- register_and_payload.py | 64 ++++++++++++++++++++ src/harp-protocol/harp/protocol/_payload.py | 4 +- src/harp-protocol/harp/protocol/_register.py | 2 +- tests/protocol/test_payload.py | 12 ++-- tests/protocol/test_register.py | 33 ++++------ 5 files changed, 84 insertions(+), 31 deletions(-) create mode 100644 register_and_payload.py diff --git a/register_and_payload.py b/register_and_payload.py new file mode 100644 index 0000000..a58c778 --- /dev/null +++ b/register_and_payload.py @@ -0,0 +1,64 @@ +"""Example: working with registers and payloads. + +Shows how to construct, encode, and parse both a simple scalar register +(WhoAmI) and a structured register (OperationControl) — for a single +sample and for 5 concatenated samples (e.g. from a bulk read). + +No device connection is required; everything runs offline. +""" + +import numpy as np + +from harp.device._registers import OperationControl, OperationControlPayload, WhoAmI + +# ── Single OperationControlPayload ─────────────────────────────────────────── + +print("=== Single OperationControlPayload ===") + +p = OperationControlPayload( + operation_mode=1, # Active + dump_registers=False, + mute_replies=False, + heartbeat=True, +) +print(p) +print(f" operation_mode : {p.operation_mode[0]}") +print(f" heartbeat : {p.heartbeat[0]}") + +# Build a Write frame for this payload +write_frame = OperationControl.format(p) +print(f" write frame : {write_frame}") + +# Build a Read request frame (no payload needed) +read_frame = OperationControl.format() +print(f" read frame : {read_frame}") + +# Round-trip: encode to bytes and parse back +raw = p.raw_payload.tobytes() +recovered = OperationControl.parse(raw) +print(f" recovered : {recovered}") + +# ── 5 concatenated OperationControlPayloads ────────────────────────────────── + +print("\n=== 5 concatenated OperationControlPayloads ===") + +samples = [OperationControlPayload(operation_mode=i % 3, heartbeat=bool(i % 2)) for i in range(5)] +bulk_bytes = b"".join(s.raw_payload.tobytes() for s in samples) + +bulk = OperationControl.parse_bulk(bulk_bytes) +print(f" samples : {len(bulk)}") +print(f" operation_mode : {bulk.operation_mode}") +print(f" heartbeat : {bulk.heartbeat}") +print(bulk.to_dataframe()) + +# ── WhoAmI (plain scalar register) ─────────────────────────────────────────── + +print("\n=== WhoAmI (scalar register) ===") + +read_frame = WhoAmI.format() +print(f" read frame : {read_frame.hex()}") + +# Simulate a raw response payload (device id = 1234) +raw_who_am_i = np.array([1234], dtype=np.uint16).tobytes() +who = WhoAmI.parse(raw_who_am_i) +print(f" device id : {who.value}") diff --git a/src/harp-protocol/harp/protocol/_payload.py b/src/harp-protocol/harp/protocol/_payload.py index b451496..c84cba7 100644 --- a/src/harp-protocol/harp/protocol/_payload.py +++ b/src/harp-protocol/harp/protocol/_payload.py @@ -68,12 +68,10 @@ def from_buffer(cls, buf: bytes | bytearray | memoryview) -> Self: @property def value(self) -> NpStructT: """Returns a single scalar if the array has one element, otherwise the full array.""" - if len(self._arr) == 1: - return self._arr[0] # type: ignore[return-value] return self._arr # type: ignore[return-value] # ty: ignore[invalid-return-type] @property - def payload(self) -> NDArray[np.void]: + def raw_payload(self) -> NDArray[np.void]: """Raw structured numpy array.""" return self._arr diff --git a/src/harp-protocol/harp/protocol/_register.py b/src/harp-protocol/harp/protocol/_register.py index a58041b..1934704 100644 --- a/src/harp-protocol/harp/protocol/_register.py +++ b/src/harp-protocol/harp/protocol/_register.py @@ -120,7 +120,7 @@ def format( mt = MessageType.Write if message_type is None else message_type if isinstance(value, PayloadBase): # Payload instance — use its backing array bytes directly - raw = value.payload.tobytes() + raw = value.raw_payload.tobytes() elif isinstance(value, np.ndarray) and value.dtype != cls.payload_type.numpy_dtype: # Structured numpy array passed by hand raw = value.tobytes() diff --git a/tests/protocol/test_payload.py b/tests/protocol/test_payload.py index e16be06..2833c98 100644 --- a/tests/protocol/test_payload.py +++ b/tests/protocol/test_payload.py @@ -12,11 +12,11 @@ class SimplePayload(PayloadBase): @property def x(self) -> NDArray[np.int16]: - return self.payload["x"] + return self.raw_payload["x"] @property def y(self) -> NDArray[np.uint8]: - return self.payload["y"] + return self.raw_payload["y"] class BitPackedPayload(PayloadBase): @@ -25,8 +25,8 @@ class BitPackedPayload(PayloadBase): def to_dataframe(self) -> pd.DataFrame: return pd.DataFrame( { - "flag_a": (self.payload["packed"] & 0x01).astype(bool), - "flag_b": ((self.payload["packed"] >> 1) & 0x01).astype(bool), + "flag_a": (self.raw_payload["packed"] & 0x01).astype(bool), + "flag_b": ((self.raw_payload["packed"] >> 1) & 0x01).astype(bool), } ) @@ -72,9 +72,9 @@ def test_from_buffer_zero_copy(): p = SimplePayload.from_buffer(data) # np.frombuffer returns a read-only view — writes should raise with pytest.raises((ValueError, TypeError)): - p.payload["x"][0] = 999 + p.raw_payload["x"][0] = 999 def test_payload_property(): p = SimplePayload.from_buffer(_make_simple_bytes(2)) - assert p.payload.dtype == SimplePayload._dtype + assert p.raw_payload.dtype == SimplePayload._dtype diff --git a/tests/protocol/test_register.py b/tests/protocol/test_register.py index c487511..7758ad8 100644 --- a/tests/protocol/test_register.py +++ b/tests/protocol/test_register.py @@ -137,7 +137,7 @@ def test_named_register_roundtrip(value): msg = _parse_frame(frame) parsed = TimestampSecond.parse(msg) assert isinstance(parsed, PayloadU32) - assert int(parsed.value) == value + assert parsed.value == np.array([value]) def test_named_register_format_read_frame(): @@ -159,7 +159,7 @@ def test_factory_format_and_parse(): msg = _parse_frame(frame) parsed = reg.parse(msg) assert isinstance(parsed, PayloadU32) - assert int(parsed.value) == 100 + assert parsed.value == np.array([100]) def test_factory_different_addresses_are_independent(): @@ -186,7 +186,7 @@ def test_format_with_payload_instance(reg_cls, payload_cls, value): frame = reg.format(payload) msg = _parse_frame(frame) assert msg.message_type == MessageType.Write - assert msg.payload == payload.payload.tobytes() + assert msg.payload == payload.raw_payload.tobytes() def test_format_with_payload_instance_via_register(): @@ -195,7 +195,7 @@ def test_format_with_payload_instance_via_register(): frame = TimestampSecond.format(payload) msg = _parse_frame(frame) parsed = TimestampSecond.parse(msg) - assert int(parsed.value) == 42 + assert parsed.value == np.array([42]) def test_structured_register_parse_bulk(): @@ -286,7 +286,7 @@ def test_array_register_parse_roundtrip(): msg = _parse_frame(frame) parsed = reg.parse(msg) # The payload array contains the packed sub-array as a single element - flat = parsed.payload.flatten().view(np.dtype(" Date: Wed, 29 Apr 2026 15:03:00 -0700 Subject: [PATCH 196/267] Delete unused file --- register_and_payload.py | 64 ----------------------------------------- 1 file changed, 64 deletions(-) delete mode 100644 register_and_payload.py diff --git a/register_and_payload.py b/register_and_payload.py deleted file mode 100644 index a58c778..0000000 --- a/register_and_payload.py +++ /dev/null @@ -1,64 +0,0 @@ -"""Example: working with registers and payloads. - -Shows how to construct, encode, and parse both a simple scalar register -(WhoAmI) and a structured register (OperationControl) — for a single -sample and for 5 concatenated samples (e.g. from a bulk read). - -No device connection is required; everything runs offline. -""" - -import numpy as np - -from harp.device._registers import OperationControl, OperationControlPayload, WhoAmI - -# ── Single OperationControlPayload ─────────────────────────────────────────── - -print("=== Single OperationControlPayload ===") - -p = OperationControlPayload( - operation_mode=1, # Active - dump_registers=False, - mute_replies=False, - heartbeat=True, -) -print(p) -print(f" operation_mode : {p.operation_mode[0]}") -print(f" heartbeat : {p.heartbeat[0]}") - -# Build a Write frame for this payload -write_frame = OperationControl.format(p) -print(f" write frame : {write_frame}") - -# Build a Read request frame (no payload needed) -read_frame = OperationControl.format() -print(f" read frame : {read_frame}") - -# Round-trip: encode to bytes and parse back -raw = p.raw_payload.tobytes() -recovered = OperationControl.parse(raw) -print(f" recovered : {recovered}") - -# ── 5 concatenated OperationControlPayloads ────────────────────────────────── - -print("\n=== 5 concatenated OperationControlPayloads ===") - -samples = [OperationControlPayload(operation_mode=i % 3, heartbeat=bool(i % 2)) for i in range(5)] -bulk_bytes = b"".join(s.raw_payload.tobytes() for s in samples) - -bulk = OperationControl.parse_bulk(bulk_bytes) -print(f" samples : {len(bulk)}") -print(f" operation_mode : {bulk.operation_mode}") -print(f" heartbeat : {bulk.heartbeat}") -print(bulk.to_dataframe()) - -# ── WhoAmI (plain scalar register) ─────────────────────────────────────────── - -print("\n=== WhoAmI (scalar register) ===") - -read_frame = WhoAmI.format() -print(f" read frame : {read_frame.hex()}") - -# Simulate a raw response payload (device id = 1234) -raw_who_am_i = np.array([1234], dtype=np.uint16).tobytes() -who = WhoAmI.parse(raw_who_am_i) -print(f" device id : {who.value}") From 2f8aec989fe6ef166a93a434f24cff72fa432bed Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:08:09 -0700 Subject: [PATCH 197/267] Raise error on returned messages with error bit flag --- src/harp-device/src/harp/device/_serial.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/harp-device/src/harp/device/_serial.py b/src/harp-device/src/harp/device/_serial.py index b81201f..aa53b4a 100644 --- a/src/harp-device/src/harp/device/_serial.py +++ b/src/harp-device/src/harp/device/_serial.py @@ -15,7 +15,9 @@ class SerialDevice: DEFAULT_BAUDRATE: ClassVar[int] = 1_000_000 REPLY_TIMEOUT: float = 5.0 # seconds - def __init__(self, port: str, baudrate: int = DEFAULT_BAUDRATE) -> None: + def __init__( + self, port: str, baudrate: int = DEFAULT_BAUDRATE, raise_on_error: bool = True + ) -> None: self._serial = serial.Serial(port, baudrate, timeout=0.1) self._serial.dtr = True @@ -23,6 +25,7 @@ def __init__(self, port: str, baudrate: int = DEFAULT_BAUDRATE) -> None: self._pending: dict[int, queue.SimpleQueue] = {} self._pending_lock = threading.Lock() self._running = True + self.raise_on_error = raise_on_error self._thread = threading.Thread( target=self._read_loop, daemon=True, name="harp-serial-device" @@ -86,6 +89,12 @@ def _dispatch(self, msg: HarpMessage) -> None: q.put(msg) elif msg.message_type == MessageType.Event: self._on_event(msg) + else: + # Unknown message type + with self._pending_lock: + q = self._pending.get(msg.address) + if q is not None: + q.put(msg) def _request(self, address: int, frame: bytes) -> HarpMessage: q: queue.SimpleQueue = queue.SimpleQueue() @@ -94,7 +103,13 @@ def _request(self, address: int, frame: bytes) -> HarpMessage: try: self._serial.write(frame) try: - return q.get(timeout=self.REPLY_TIMEOUT) + msg = q.get(timeout=self.REPLY_TIMEOUT) + if msg.has_error and self.raise_on_error: + raise RuntimeError( + f"Device returned error for register address {address} " + f"(0x{address:02x}). Payload: {msg.payload.hex()}" + ) + return msg except queue.Empty as exc: raise TimeoutError( f"No reply from device for register address {address} " From ed599b3ff6ee01d82a7cccbec5cc79e3bb4263f2 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Wed, 29 Apr 2026 19:30:41 -0700 Subject: [PATCH 198/267] Add timestamp to format Co-authored-by: Copilot --- src/harp-device/src/harp/device/_serial.py | 19 +++++-- src/harp-protocol/harp/protocol/_register.py | 46 +++------------ tests/protocol/test_register.py | 60 ++++++++------------ 3 files changed, 47 insertions(+), 78 deletions(-) diff --git a/src/harp-device/src/harp/device/_serial.py b/src/harp-device/src/harp/device/_serial.py index aa53b4a..cc314e7 100644 --- a/src/harp-device/src/harp/device/_serial.py +++ b/src/harp-device/src/harp/device/_serial.py @@ -32,13 +32,24 @@ def __init__( ) self._thread.start() - def read_register(self, register: type[RegisterBase[P]]) -> ParsedHarpMessage[P]: - frame = register.format() + def read_register( + self, register: type[RegisterBase[P]], *, timestamp: float | None = None, port: int = 255 + ) -> ParsedHarpMessage[P]: + frame = register.format(message_type=MessageType.Read, timestamp=timestamp, port=port) msg = self._request(register.address, frame) return ParsedHarpMessage.from_message(msg, register.parse(msg)) - def write_register(self, register: type[RegisterBase[P]], value: Any) -> ParsedHarpMessage[P]: - frame = register.format(value) + def write_register( + self, + register: type[RegisterBase[P]], + value: Any, + *, + timestamp: float | None = None, + port: int = 255, + ) -> ParsedHarpMessage[P]: + frame = register.format( + value, message_type=MessageType.Write, timestamp=timestamp, port=port + ) msg = self._request(register.address, frame) return ParsedHarpMessage.from_message(msg, register.parse(msg)) diff --git a/src/harp-protocol/harp/protocol/_register.py b/src/harp-protocol/harp/protocol/_register.py index 1934704..b2da9b8 100644 --- a/src/harp-protocol/harp/protocol/_register.py +++ b/src/harp-protocol/harp/protocol/_register.py @@ -77,19 +77,13 @@ def parse(cls, value: HarpMessage | bytes | bytearray | memoryview) -> P: buf = value.payload if isinstance(value, HarpMessage) else value return cast(P, cls.payload_class.from_buffer(buf)) - @classmethod - def parse_bulk(cls, data: bytes | bytearray | memoryview | list[HarpMessage]) -> P: - """Parse a batch of payloads from concatenated bytes or a list of messages.""" - if isinstance(data, list): - data = b"".join(msg.payload for msg in data) - return cast(P, cls.payload_class.from_buffer(data)) - @overload @classmethod def format( cls, *, message_type: MessageType = MessageType.Read, + timestamp: float | None = None, port: int = 0xFF, ) -> bytes: ... @@ -100,6 +94,7 @@ def format( value: Any, *, message_type: MessageType = MessageType.Write, + timestamp: float | None = None, port: int = 0xFF, ) -> bytes: ... @@ -110,12 +105,15 @@ def format( value: Any = _MISSING, *, message_type: MessageType | None = None, + timestamp: float | None = None, port: int = 0xFF, ) -> bytes: """Build a Harp frame for this register. No value → Read; with value → Write.""" if value is _MISSING: mt = MessageType.Read if message_type is None else message_type - return build_message_frame(mt, cls.address, cls.payload_type, port=port) + return build_message_frame( + mt, cls.address, cls.payload_type, port=port, timestamp=timestamp + ) else: mt = MessageType.Write if message_type is None else message_type if isinstance(value, PayloadBase): @@ -127,20 +125,9 @@ def format( else: # Scalar or array castable to the register's primitive dtype raw = np.asarray(value, dtype=cls.payload_type.numpy_dtype).tobytes() - return build_message_frame(mt, cls.address, cls.payload_type, raw, port=port) - - -# ------------------------------------------------------------------ -# Typed scalar classes — one per PayloadType. -# These can be used both as base classes for named registers: -# -# class TimestampSecond(RegisterU32): -# address: ClassVar[int] = 8 -# -# and as on-the-fly register classes for raw addresses: -# -# dev.read(RegisterU32(0x08)) -# ------------------------------------------------------------------ + return build_message_frame( + mt, cls.address, cls.payload_type, raw, port=port, timestamp=timestamp + ) class RegisterU8(RegisterBase[PayloadU8], metaclass=_RegisterMeta): @@ -188,21 +175,6 @@ class RegisterFloat(RegisterBase[PayloadFloat], metaclass=_RegisterMeta): payload_class: ClassVar[type[PayloadFloat]] = PayloadFloat -# ------------------------------------------------------------------ -# Array register classes — fixed-length element arrays per message. -# -# Named register usage: -# -# class DigitalOutputs(RegisterU16Array): -# address: ClassVar[int] = 0x28 -# length: ClassVar[int] = 3 -# -# On-the-fly usage: -# -# dev.read(RegisterU16Array(0x28, length=3)) -# ------------------------------------------------------------------ - - class _ArrayRegisterMeta(ABCMeta): """Calling with address and length creates a concrete subclass: ``RegisterU16Array(0x28, length=3)``.""" diff --git a/tests/protocol/test_register.py b/tests/protocol/test_register.py index 7758ad8..0de47ce 100644 --- a/tests/protocol/test_register.py +++ b/tests/protocol/test_register.py @@ -198,40 +198,6 @@ def test_format_with_payload_instance_via_register(): assert parsed.value == np.array([42]) -def test_structured_register_parse_bulk(): - raw = np.array( - [(100, 512, -200), (110, 513, -210), (120, 514, -220)], - dtype=AnalogDataPayload._dtype, - ).tobytes() - - bulk = AnalogData.parse_bulk(raw) - assert isinstance(bulk, AnalogDataPayload) - assert len(bulk) == 3 - np.testing.assert_array_equal(bulk.analog_input0, [100, 110, 120]) - np.testing.assert_array_equal(bulk.encoder, [512, 513, 514]) - np.testing.assert_array_equal(bulk.analog_input1, [-200, -210, -220]) - - -def test_structured_register_parse_from_message_list(): - records = [(100, 512, -200), (110, 513, -210)] - messages = [] - for rec in records: - payload_bytes = np.array([rec], dtype=AnalogDataPayload._dtype).tobytes() - msg = HarpMessage( - MessageType.Event, - AnalogData.address, - AnalogData.payload_type, - payload_bytes, - ) - messages.append(msg) - - bulk = AnalogData.parse_bulk(messages) - assert len(bulk) == 2 - np.testing.assert_array_equal(bulk.analog_input0, [100, 110]) - np.testing.assert_array_equal(bulk.encoder, [512, 513]) - np.testing.assert_array_equal(bulk.analog_input1, [-200, -210]) - - def test_structured_register_format_single_sample(): sample = np.array([(100, 512, -200)], dtype=AnalogDataPayload._dtype) frame = AnalogData.format(sample) @@ -248,7 +214,7 @@ def test_structured_register_to_dataframe(): [(1, 2, 3), (4, 5, 6)], dtype=AnalogDataPayload._dtype, ).tobytes() - bulk = AnalogData.parse_bulk(raw) + bulk = AnalogData.parse(raw) df = bulk.to_dataframe() assert list(df.columns) == ["analog_input0", "encoder", "analog_input1"] assert len(df) == 2 @@ -330,6 +296,26 @@ def test_format_write_override_message_type(): assert msg.message_type == MessageType.Event +def test_format_read_with_timestamp(): + ts = 12.5 + frame = TimestampSecond.format(timestamp=ts) + msg = _parse_frame(frame) + assert msg.message_type == MessageType.Read + assert msg.has_timestamp + assert msg.timestamp == pytest.approx(ts, abs=1e-4) + + +def test_format_write_with_timestamp(): + ts = 100.0 + frame = TimestampSecond.format(42, timestamp=ts) + msg = _parse_frame(frame) + assert msg.message_type == MessageType.Write + assert msg.has_timestamp + assert msg.timestamp == pytest.approx(ts, abs=1e-4) + parsed = TimestampSecond.parse(msg) + assert parsed.value == 42 + + # --------------------------------------------------------------------------- # 9. .value property # --------------------------------------------------------------------------- @@ -419,9 +405,9 @@ def test_array_register_value_single(): def test_array_register_value_multi(): """.value on a multi-record array-register payload returns the full 2-D array.""" reg = RegisterU32Array(0x08, length=3) - # Two rows of 3 elements each; pass as flat bytes via parse_bulk + # Two rows of 3 elements each; pass as flat bytes rows = np.array([[10, 20, 30], [40, 50, 60]], dtype=np.dtype(" len == 2 => .value returns the full array assert len(bulk) == 2 v = bulk.value From 2e6f5ed2790d2092f84396f8c55ac87767c5bef2 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Thu, 30 Apr 2026 12:44:06 -0700 Subject: [PATCH 199/267] Add example script and refactor payload classes for clarity and consistency --- core_registers_example.py | 69 +++++ src/harp-device/src/harp/device/_registers.py | 274 +++++++++++++++--- src/harp-protocol/harp/protocol/_payload.py | 32 +- 3 files changed, 327 insertions(+), 48 deletions(-) create mode 100644 core_registers_example.py diff --git a/core_registers_example.py b/core_registers_example.py new file mode 100644 index 0000000..214041d --- /dev/null +++ b/core_registers_example.py @@ -0,0 +1,69 @@ +"""Example: construct and parse every core Harp register payload. + +Run with: + uv run python core_registers_example.py +""" + +import numpy as np + +from harp.device._registers import ( + # Enums / flags + EnableFlag, + OperationMode, + # Payload classes + ClockConfigPayload, + DeviceNamePayload, + OperationControlPayload, + ResetDevicePayload, + # Registers + AssemblyVersion, + ClockConfig, + CoreVersionH, + CoreVersionL, + DeviceName, + FirmwareVersionH, + FirmwareVersionL, + Heartbeat, + HwVersionH, + HwVersionL, + OperationControl, + ResetDevice, + SerialNumber, + TimestampMicro, + TimestampOffset, + TimestampSecond, + WhoAmI, +) + +from harp.protocol._message import HarpMessage + +examples = [ + (WhoAmI, np.uint16(1216)), + (TimestampSecond, np.uint32(3600)), + (TimestampMicro, np.uint16(500)), + (OperationControl, OperationControlPayload( + operation_mode=OperationMode.Active, + dump_registers=True, + visual_indicators=EnableFlag.Enabled, + operation_led=EnableFlag.Enabled, + heartbeat=EnableFlag.Enabled, + )), + (ResetDevice, ResetDevicePayload(restore_default=True, restore_name=True)), + (DeviceName, DeviceNamePayload("my-harp-device")), + (ClockConfig, ClockConfigPayload(clock_repeater=True, clock_unlock=True)), + (Heartbeat, np.uint16(1)), + (HwVersionH, np.uint8(2)), + (HwVersionL, np.uint8(0)), + (AssemblyVersion, np.uint8(3)), + (CoreVersionH, np.uint8(1)), + (CoreVersionL, np.uint8(4)), + (FirmwareVersionH, np.uint8(2)), + (FirmwareVersionL, np.uint8(1)), + (SerialNumber, np.uint16(42)), + (TimestampOffset, np.uint8(0)), +] + +for register, value in examples: + frame = register.format(value) + parsed = register.parse(HarpMessage.parse(frame)) + print(f"{register.__name__:20s} (addr {register.address:2d}) → {parsed}") diff --git a/src/harp-device/src/harp/device/_registers.py b/src/harp-device/src/harp/device/_registers.py index 2744988..4ed86c4 100644 --- a/src/harp-device/src/harp/device/_registers.py +++ b/src/harp-device/src/harp/device/_registers.py @@ -6,18 +6,29 @@ from harp.protocol._payload import PayloadBase from harp.protocol._payload_type import PayloadType from harp.protocol._register import RegisterBase, RegisterU8, RegisterU16, RegisterU32 -from numpy.typing import NDArray - class OperationMode(enum.IntEnum): + """Specifies the operation mode of the device.""" + Standby = 0 Active = 1 - Speed = 2 - Reserved = 3 + Speed = 3 + + +class EnableFlag(enum.IntEnum): + """Specifies whether a specific register flag is enabled or disabled.""" + + Disabled = 0 + Enabled = 1 + +# --------------------------------------------------------------------------- +# Complex payload classes +# --------------------------------------------------------------------------- -class OperationControlPayload(PayloadBase[np.void]): - _dtype: ClassVar = np.dtype([("operation_control", "u1")]) + +class OperationControlPayload(PayloadBase[np.uint8]): + _dtype: ClassVar = np.dtype("u1") _repr_fields: ClassVar = ( "operation_mode", "dump_registers", @@ -30,59 +41,234 @@ class OperationControlPayload(PayloadBase[np.void]): def __init__( self, *, - operation_mode: int | np.uint8 = 0, + operation_mode: OperationMode | int = OperationMode.Standby, dump_registers: bool = False, mute_replies: bool = False, - visual_indicators: int | np.uint8 = 0, - operation_led: int | np.uint8 = 0, - heartbeat: int | np.uint8 = 0, + visual_indicators: EnableFlag | int = EnableFlag.Disabled, + operation_led: EnableFlag | int = EnableFlag.Disabled, + heartbeat: EnableFlag | int = EnableFlag.Disabled, + ) -> None: + val = np.uint8(0) + val |= np.uint8(operation_mode) & np.uint8(0x03) + val |= np.uint8(dump_registers) << np.uint8(3) + val |= np.uint8(mute_replies) << np.uint8(4) + val |= (np.uint8(visual_indicators) & np.uint8(0x01)) << np.uint8(5) + val |= (np.uint8(operation_led) & np.uint8(0x01)) << np.uint8(6) + val |= (np.uint8(heartbeat) & np.uint8(0x01)) << np.uint8(7) + self._arr = np.array([val], dtype=self._dtype) + + @property + def operation_mode(self) -> np.ndarray: + return self._arr & np.uint8(0x03) + + @property + def dump_registers(self) -> np.ndarray: + return (self._arr >> 3) & np.uint8(0x01) + + @property + def mute_replies(self) -> np.ndarray: + return (self._arr >> 4) & np.uint8(0x01) + + @property + def visual_indicators(self) -> np.ndarray: + return (self._arr >> 5) & np.uint8(0x01) + + @property + def operation_led(self) -> np.ndarray: + return (self._arr >> 6) & np.uint8(0x01) + + @property + def heartbeat(self) -> np.ndarray: + return (self._arr >> 7) & np.uint8(0x01) + + def to_dataframe(self) -> pd.DataFrame: + return pd.DataFrame( + { + "operation_mode": [self.operation_mode], + "dump_registers": [self.dump_registers], + "mute_replies": [self.mute_replies], + "visual_indicators": [self.visual_indicators], + "operation_led": [self.operation_led], + "heartbeat": [self.heartbeat], + } + ) + + +class ResetDevicePayload(PayloadBase[np.uint8]): + """Payload for the ResetDevice register (address 11).""" + + _dtype: ClassVar = np.dtype("u1") + _repr_fields: ClassVar = ( + "restore_default", + "restore_eeprom", + "save", + "restore_name", + "update_firmware", + "boot_from_default", + "boot_from_eeprom", + ) + + def __init__( + self, + *, + restore_default: bool = False, + restore_eeprom: bool = False, + save: bool = False, + restore_name: bool = False, + update_firmware: bool = False, + boot_from_default: bool = False, + boot_from_eeprom: bool = False, ) -> None: - arr = np.zeros(1, dtype=self._dtype) - arr["operation_control"] |= np.uint8(operation_mode) & 0x03 - arr["operation_control"] |= np.uint8(dump_registers) << 3 - arr["operation_control"] |= np.uint8(mute_replies) << 4 - arr["operation_control"] |= (np.uint8(visual_indicators) & 0x01) << 5 - arr["operation_control"] |= (np.uint8(operation_led) & 0x01) << 6 - arr["operation_control"] |= (np.uint8(heartbeat) & 0x01) << 7 - self._arr = arr + val = np.uint8(0) + val |= np.uint8(restore_default) << np.uint8(0) + val |= np.uint8(restore_eeprom) << np.uint8(1) + val |= np.uint8(save) << np.uint8(2) + val |= np.uint8(restore_name) << np.uint8(3) + val |= np.uint8(update_firmware) << np.uint8(5) + val |= np.uint8(boot_from_default) << np.uint8(6) + val |= np.uint8(boot_from_eeprom) << np.uint8(7) + self._arr = np.array([val], dtype=self._dtype) + + @property + def restore_default(self) -> bool: + return bool(self._arr & np.uint8(0x01)) @property - def operation_mode(self) -> NDArray[np.uint8]: - return self._arr["operation_control"] & np.uint8(0x03) + def restore_eeprom(self) -> bool: + return bool(self._arr & np.uint8(0x02)) @property - def dump_registers(self) -> NDArray[np.bool_]: - return (self._arr["operation_control"] & np.uint8(0x08)) != 0 + def save(self) -> bool: + return bool(self._arr & np.uint8(0x04)) @property - def mute_replies(self) -> NDArray[np.bool_]: - return (self._arr["operation_control"] & np.uint8(0x10)) != 0 + def restore_name(self) -> bool: + return bool(self._arr & np.uint8(0x08)) @property - def visual_indicators(self) -> NDArray[np.bool_]: - return ((self._arr["operation_control"] >> 5) & np.uint8(0x01)) != 0 + def update_firmware(self) -> bool: + return bool(self._arr & np.uint8(0x20)) @property - def operation_led(self) -> NDArray[np.bool_]: - return ((self._arr["operation_control"] >> 6) & np.uint8(0x01)) != 0 + def boot_from_default(self) -> bool: + return bool(self._arr & np.uint8(0x40)) @property - def heartbeat(self) -> NDArray[np.bool_]: - return ((self._arr["operation_control"] >> 7) & np.uint8(0x01)) != 0 + def boot_from_eeprom(self) -> bool: + return bool(self._arr & np.uint8(0x80)) def to_dataframe(self) -> pd.DataFrame: return pd.DataFrame( { - "operation_mode": self.operation_mode, - "dump_registers": self.dump_registers, - "mute_replies": self.mute_replies, - "visual_indicators": self.visual_indicators, - "operation_led": self.operation_led, - "heartbeat": self.heartbeat, + "restore_default": self.restore_default, + "restore_eeprom": self.restore_eeprom, + "save": self.save, + "restore_name": self.restore_name, + "update_firmware": self.update_firmware, + "boot_from_default": self.boot_from_default, + "boot_from_eeprom": self.boot_from_eeprom, } ) +class DeviceNamePayload(PayloadBase[np.uint8]): + """Payload for the DeviceName register (address 12). + + Stores a user-specified ASCII device name padded to 25 bytes. + Access the decoded string via ``.name``; ``.value`` returns the raw byte array. + """ + + _dtype: ClassVar = np.dtype("u1") + _repr_fields: ClassVar = ("name",) + _MAX_LEN: ClassVar[int] = 25 + + def __init__(self, name: str) -> None: + encoded = name.encode("ascii")[: self._MAX_LEN] + padded = encoded.ljust(self._MAX_LEN, b"\x00") + self._arr = np.frombuffer(padded, dtype=self._dtype).copy() + + @property + def name(self) -> str: + return self._arr.tobytes().rstrip(b"\x00").decode("ascii") + + def to_dataframe(self) -> pd.DataFrame: + return pd.DataFrame({"name": [self.name]}) + + +class ClockConfigPayload(PayloadBase[np.uint8]): + """Payload for the ClockConfiguration register (address 14).""" + + _dtype: ClassVar = np.dtype("u1") + _repr_fields: ClassVar = ( + "clock_repeater", + "clock_generator", + "repeater_capability", + "generator_capability", + "clock_unlock", + "clock_lock", + ) + + def __init__( + self, + *, + clock_repeater: bool = False, + clock_generator: bool = False, + repeater_capability: bool = False, + generator_capability: bool = False, + clock_unlock: bool = False, + clock_lock: bool = False, + ) -> None: + val = np.uint8(0) + val |= np.uint8(clock_repeater) << np.uint8(0) + val |= np.uint8(clock_generator) << np.uint8(1) + val |= np.uint8(repeater_capability) << np.uint8(3) + val |= np.uint8(generator_capability) << np.uint8(4) + val |= np.uint8(clock_unlock) << np.uint8(6) + val |= np.uint8(clock_lock) << np.uint8(7) + self._arr = np.array([val], dtype=self._dtype) + + @property + def clock_repeater(self) -> np.ndarray: + return self._arr & np.uint8(0x01) + + @property + def clock_generator(self) -> np.ndarray: + return self._arr & np.uint8(0x02) + + @property + def repeater_capability(self) -> np.ndarray: + return self._arr & np.uint8(0x08) + + @property + def generator_capability(self) -> np.ndarray: + return self._arr & np.uint8(0x10) + + @property + def clock_unlock(self) -> np.ndarray: + return self._arr & np.uint8(0x40) + + @property + def clock_lock(self) -> np.ndarray: + return self._arr & np.uint8(0x80) + + def to_dataframe(self) -> pd.DataFrame: + return pd.DataFrame( + { + "clock_repeater": [self.clock_repeater], + "clock_generator": [self.clock_generator], + "repeater_capability": [self.repeater_capability], + "generator_capability": [self.generator_capability], + "clock_unlock": [self.clock_unlock], + "clock_lock": [self.clock_lock], + } + ) + + +# --------------------------------------------------------------------------- +# Registers +# --------------------------------------------------------------------------- + + class WhoAmI(RegisterU16): address: ClassVar[int] = 0 @@ -101,12 +287,22 @@ class OperationControl(RegisterBase[OperationControlPayload]): payload_class: ClassVar[type[PayloadBase]] = OperationControlPayload -class ResetDevice(RegisterU8): +class ResetDevice(RegisterBase[ResetDevicePayload]): address: ClassVar[int] = 11 + payload_type: ClassVar[PayloadType] = PayloadType.U8 + payload_class: ClassVar[type[PayloadBase]] = ResetDevicePayload + + +class DeviceName(RegisterBase[DeviceNamePayload]): + address: ClassVar[int] = 12 + payload_type: ClassVar[PayloadType] = PayloadType.U8 + payload_class: ClassVar[type[PayloadBase]] = DeviceNamePayload -class ClockConfig(RegisterU8): +class ClockConfig(RegisterBase[ClockConfigPayload]): address: ClassVar[int] = 14 + payload_type: ClassVar[PayloadType] = PayloadType.U8 + payload_class: ClassVar[type[PayloadBase]] = ClockConfigPayload class Heartbeat(RegisterU16): @@ -150,5 +346,3 @@ class SerialNumber(RegisterU16): class TimestampOffset(RegisterU8): address: ClassVar[int] = 15 - address: ClassVar[int] = 15 - address: ClassVar[int] = 15 diff --git a/src/harp-protocol/harp/protocol/_payload.py b/src/harp-protocol/harp/protocol/_payload.py index c84cba7..abdf850 100644 --- a/src/harp-protocol/harp/protocol/_payload.py +++ b/src/harp-protocol/harp/protocol/_payload.py @@ -6,8 +6,7 @@ import pandas as pd from numpy.typing import NDArray -NpStructT = TypeVar("NpStructT", bound=np.generic | np.ndarray) - +NpStructT = TypeVar("NpStructT", bound=np.generic) class PayloadBase(Generic[NpStructT]): """Base class for typed Harp register payloads. @@ -20,6 +19,23 @@ class AnalogDataPayload(PayloadBase): ("encoder", " None: """Construct a single-sample payload. Structured dtypes use keyword arguments; scalar dtypes use a single positional argument.""" @@ -66,13 +82,13 @@ def from_buffer(cls, buf: bytes | bytearray | memoryview) -> Self: return obj @property - def value(self) -> NpStructT: - """Returns a single scalar if the array has one element, otherwise the full array.""" - return self._arr # type: ignore[return-value] # ty: ignore[invalid-return-type] + def value(self) -> NDArray[NpStructT]: + """Returns the backing array of ``_dtype`` records.""" + return self._arr @property - def raw_payload(self) -> NDArray[np.void]: - """Raw structured numpy array.""" + def raw_payload(self) -> NDArray[NpStructT]: + """Raw structured numpy array (alias for ``value``; useful for explicit byte serialisation).""" return self._arr def to_dataframe(self) -> pd.DataFrame: From a66372d1ab267266d7c6140f6a3180f3a3eed381 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sat, 9 May 2026 16:27:43 -0700 Subject: [PATCH 200/267] Add BitField types --- src/harp-device/src/harp/device/_device.py | 1 - src/harp-device/src/harp/device/_registers.py | 137 ++------ src/harp-protocol/harp/protocol/_builder.py | 1 - src/harp-protocol/harp/protocol/_payload.py | 55 ++- src/harp-protocol/harp/protocol/_register.py | 63 ++++ tests/test_device.py | 315 ++++++++++++------ 6 files changed, 357 insertions(+), 215 deletions(-) diff --git a/src/harp-device/src/harp/device/_device.py b/src/harp-device/src/harp/device/_device.py index 2548e18..aa60cf8 100644 --- a/src/harp-device/src/harp/device/_device.py +++ b/src/harp-device/src/harp/device/_device.py @@ -31,4 +31,3 @@ def __enter__(self) -> "Device": def __exit__(self, *args: object) -> None: self.close() - self.close() diff --git a/src/harp-device/src/harp/device/_registers.py b/src/harp-device/src/harp/device/_registers.py index 4ed86c4..6eec654 100644 --- a/src/harp-device/src/harp/device/_registers.py +++ b/src/harp-device/src/harp/device/_registers.py @@ -3,7 +3,7 @@ import numpy as np import pandas as pd -from harp.protocol._payload import PayloadBase +from harp.protocol._payload import PayloadBase, _BitFlag, _GroupMask from harp.protocol._payload_type import PayloadType from harp.protocol._register import RegisterBase, RegisterU8, RegisterU16, RegisterU32 @@ -38,6 +38,13 @@ class OperationControlPayload(PayloadBase[np.uint8]): "heartbeat", ) + operation_mode = _GroupMask(0x03, 0, OperationMode) + dump_registers = _BitFlag(0x08) + mute_replies = _BitFlag(0x10) + visual_indicators = _GroupMask(0x20, 5, EnableFlag) + operation_led = _GroupMask(0x40, 6, EnableFlag) + heartbeat = _GroupMask(0x80, 7, EnableFlag) + def __init__( self, *, @@ -57,42 +64,6 @@ def __init__( val |= (np.uint8(heartbeat) & np.uint8(0x01)) << np.uint8(7) self._arr = np.array([val], dtype=self._dtype) - @property - def operation_mode(self) -> np.ndarray: - return self._arr & np.uint8(0x03) - - @property - def dump_registers(self) -> np.ndarray: - return (self._arr >> 3) & np.uint8(0x01) - - @property - def mute_replies(self) -> np.ndarray: - return (self._arr >> 4) & np.uint8(0x01) - - @property - def visual_indicators(self) -> np.ndarray: - return (self._arr >> 5) & np.uint8(0x01) - - @property - def operation_led(self) -> np.ndarray: - return (self._arr >> 6) & np.uint8(0x01) - - @property - def heartbeat(self) -> np.ndarray: - return (self._arr >> 7) & np.uint8(0x01) - - def to_dataframe(self) -> pd.DataFrame: - return pd.DataFrame( - { - "operation_mode": [self.operation_mode], - "dump_registers": [self.dump_registers], - "mute_replies": [self.mute_replies], - "visual_indicators": [self.visual_indicators], - "operation_led": [self.operation_led], - "heartbeat": [self.heartbeat], - } - ) - class ResetDevicePayload(PayloadBase[np.uint8]): """Payload for the ResetDevice register (address 11).""" @@ -108,6 +79,14 @@ class ResetDevicePayload(PayloadBase[np.uint8]): "boot_from_eeprom", ) + restore_default = _BitFlag(0x01) + restore_eeprom = _BitFlag(0x02) + save = _BitFlag(0x04) + restore_name = _BitFlag(0x08) + update_firmware = _BitFlag(0x20) + boot_from_default = _BitFlag(0x40) + boot_from_eeprom = _BitFlag(0x80) + def __init__( self, *, @@ -129,47 +108,6 @@ def __init__( val |= np.uint8(boot_from_eeprom) << np.uint8(7) self._arr = np.array([val], dtype=self._dtype) - @property - def restore_default(self) -> bool: - return bool(self._arr & np.uint8(0x01)) - - @property - def restore_eeprom(self) -> bool: - return bool(self._arr & np.uint8(0x02)) - - @property - def save(self) -> bool: - return bool(self._arr & np.uint8(0x04)) - - @property - def restore_name(self) -> bool: - return bool(self._arr & np.uint8(0x08)) - - @property - def update_firmware(self) -> bool: - return bool(self._arr & np.uint8(0x20)) - - @property - def boot_from_default(self) -> bool: - return bool(self._arr & np.uint8(0x40)) - - @property - def boot_from_eeprom(self) -> bool: - return bool(self._arr & np.uint8(0x80)) - - def to_dataframe(self) -> pd.DataFrame: - return pd.DataFrame( - { - "restore_default": self.restore_default, - "restore_eeprom": self.restore_eeprom, - "save": self.save, - "restore_name": self.restore_name, - "update_firmware": self.update_firmware, - "boot_from_default": self.boot_from_default, - "boot_from_eeprom": self.boot_from_eeprom, - } - ) - class DeviceNamePayload(PayloadBase[np.uint8]): """Payload for the DeviceName register (address 12). @@ -208,6 +146,13 @@ class ClockConfigPayload(PayloadBase[np.uint8]): "clock_lock", ) + clock_repeater = _BitFlag(0x01) + clock_generator = _BitFlag(0x02) + repeater_capability = _BitFlag(0x08) + generator_capability = _BitFlag(0x10) + clock_unlock = _BitFlag(0x40) + clock_lock = _BitFlag(0x80) + def __init__( self, *, @@ -227,42 +172,6 @@ def __init__( val |= np.uint8(clock_lock) << np.uint8(7) self._arr = np.array([val], dtype=self._dtype) - @property - def clock_repeater(self) -> np.ndarray: - return self._arr & np.uint8(0x01) - - @property - def clock_generator(self) -> np.ndarray: - return self._arr & np.uint8(0x02) - - @property - def repeater_capability(self) -> np.ndarray: - return self._arr & np.uint8(0x08) - - @property - def generator_capability(self) -> np.ndarray: - return self._arr & np.uint8(0x10) - - @property - def clock_unlock(self) -> np.ndarray: - return self._arr & np.uint8(0x40) - - @property - def clock_lock(self) -> np.ndarray: - return self._arr & np.uint8(0x80) - - def to_dataframe(self) -> pd.DataFrame: - return pd.DataFrame( - { - "clock_repeater": [self.clock_repeater], - "clock_generator": [self.clock_generator], - "repeater_capability": [self.repeater_capability], - "generator_capability": [self.generator_capability], - "clock_unlock": [self.clock_unlock], - "clock_lock": [self.clock_lock], - } - ) - # --------------------------------------------------------------------------- # Registers diff --git a/src/harp-protocol/harp/protocol/_builder.py b/src/harp-protocol/harp/protocol/_builder.py index db04109..ba932d7 100644 --- a/src/harp-protocol/harp/protocol/_builder.py +++ b/src/harp-protocol/harp/protocol/_builder.py @@ -30,4 +30,3 @@ def build_message_frame( frame = header + body checksum = sum(frame) & 0xFF return frame + bytes([checksum]) - return frame + bytes([checksum]) diff --git a/src/harp-protocol/harp/protocol/_payload.py b/src/harp-protocol/harp/protocol/_payload.py index abdf850..7880794 100644 --- a/src/harp-protocol/harp/protocol/_payload.py +++ b/src/harp-protocol/harp/protocol/_payload.py @@ -8,6 +8,49 @@ NpStructT = TypeVar("NpStructT", bound=np.generic) + +class _BitFlag: + """Single-bit boolean descriptor for bitfield payload classes. + + Returns a plain ``bool`` when the payload holds a single record + (``len(_arr) == 1``), or a boolean ``np.ndarray`` for bulk reads. + """ + + def __init__(self, mask: int) -> None: + self._mask = mask + + def __set_name__(self, owner: object, name: str) -> None: + self._name = name + + def __get__(self, obj: PayloadBase | None, owner: object = None) -> bool | np.ndarray: + if obj is None: + return self # type: ignore[return-value] + v = (obj._arr & self._mask) != 0 # shape (N,) + return bool(v[0]) if len(v) == 1 else v + + +class _GroupMask: + """Multi-bit enum descriptor for bitfield payload classes. + + Returns an ``IntEnum`` member when the payload holds a single record, + or a raw integer ``np.ndarray`` for bulk reads. + """ + + def __init__(self, mask: int, shift: int, enum: type) -> None: + self._mask = mask + self._shift = shift + self._enum = enum + + def __set_name__(self, owner: object, name: str) -> None: + self._name = name + + def __get__(self, obj: PayloadBase | None, owner: object = None) -> object: + if obj is None: + return self + v = (obj._arr & self._mask) >> self._shift # shape (N,) + return self._enum(int(v[0])) if len(v) == 1 else v + + class PayloadBase(Generic[NpStructT]): """Base class for typed Harp register payloads. @@ -92,7 +135,17 @@ def raw_payload(self) -> NDArray[NpStructT]: return self._arr def to_dataframe(self) -> pd.DataFrame: - """Convert to a DataFrame. Structured dtypes produce one column per field; scalar dtypes produce a ``"value"`` column.""" + """Convert to a DataFrame. + + * Bitfield payloads with ``_repr_fields``: one column per descriptor, + shape-polymorphic (bool scalars promoted to length-1 arrays). + * Structured dtypes: one column per dtype field. + * Scalar dtypes: a single ``"value"`` column. + """ + if self._repr_fields is not None: + return pd.DataFrame( + {f: np.atleast_1d(getattr(self, f)) for f in self._repr_fields} + ) if self._dtype.names is not None: return pd.DataFrame({name: self._arr[name] for name in self._dtype.names}) return pd.DataFrame({"value": self._arr}) diff --git a/src/harp-protocol/harp/protocol/_register.py b/src/harp-protocol/harp/protocol/_register.py index b2da9b8..9a57777 100644 --- a/src/harp-protocol/harp/protocol/_register.py +++ b/src/harp-protocol/harp/protocol/_register.py @@ -11,6 +11,7 @@ class TimestampSecond(RegisterU32): """ from abc import ABC, ABCMeta +from pathlib import Path from typing import Any, ClassVar, Generic, TypeVar, cast, final, overload import numpy as np @@ -77,6 +78,68 @@ def parse(cls, value: HarpMessage | bytes | bytearray | memoryview) -> P: buf = value.payload if isinstance(value, HarpMessage) else value return cast(P, cls.payload_class.from_buffer(buf)) + @classmethod + def read_frames( + cls, + source: bytes | bytearray | memoryview | Path | str, + ) -> "tuple[np.ndarray, P]": + """Read all frames from a single-register Harp binary buffer or file. + + Parameters + ---------- + source: + Raw bytes, a bytes-like object, or a path to a ``.bin`` file + containing packed Harp frames for a single register. + + Returns + ------- + timestamps : np.ndarray + 1-D float64 array of timestamps in seconds, one per frame. + payload : P + Payload object whose ``_arr`` is a zero-copy strided view into + the raw buffer (shape ``(N,)`` for scalar/bitfield registers, + ``(N, length)`` for array registers). + """ + if isinstance(source, (str, Path)): + data = np.fromfile(source, dtype=np.uint8) + else: + data = np.frombuffer(source, dtype=np.uint8) + + if len(data) == 0: + obj = cls.payload_class.__new__(cls.payload_class) + obj._arr = np.empty(0, dtype=cls.payload_class._dtype.base) + return np.empty(0, dtype=np.float64), cast(P, obj) + + stride = int(data[1]) + 2 + nrows = len(data) // stride + is_timestamped = bool(int(data[4]) & 0x10) + payload_offset = 11 if is_timestamped else 5 + + if is_timestamped: + ts_s = np.ndarray(nrows, dtype=" None: -# # # open serial connection and load info -# # device = Device("COM74", "dump.bin") -# # assert device._ser.is_open -# # device.info() -# # device.disconnect() -# # assert not device._ser.is_open - - -# def test_read_U8() -> None: -# # open serial connection and load info -# device = Device("/dev/ttyUSB0", "dump.bin") - -# # read register 38 -# register: int = 38 -# read_size: int = 35 # TODO: automatically calculate this! - -# reply: HarpMessage = device.send( -# HarpMessage(MessageType.READ, PayloadType.U8, register) -# ) -# assert reply is not None -# # assert reply.payload == write_value - -# print(reply) -# assert device._dump_file_path.exists() -# device.disconnect() - -# # FIXME: this seems to be testing the Behavior device, not a generic harp device. -# def test_U8() -> None: -# # open serial connection and load info -# device = Device("/dev/ttyUSB0", "dump.bin") -# assert device._dump_file_path.exists() - -# register: int = 38 -# read_size: int = 20 # TODO: automatically calculate this! -# write_value: int = 65 - -# # assert reply[11] == 0 # what is the default register value?! - -# # write 65 on register 38 -# reply = device.send( -# HarpMessage( -# MessageType.WRITE, PayloadType.U8, register, write_value -# ) -# ) -# assert reply is not None - -# # read register 38 -# reply = device.read_u8(register) -# assert reply is not None -# assert reply.payload == write_value - -# device.disconnect() - - -# # def test_read_hw_version_integration() -> None: -# # -# # # serial settings -# # ser = serial.Serial( -# # "/dev/tty.usbserial-A106C8O9", -# # baudrate=1000000, -# # timeout=5, -# # parity=serial.PARITY_NONE, -# # stopbits=1, -# # bytesize=8, -# # rtscts=True, -# # ) -# # -# # assert ser.is_open -# # -# # ser.write(b"\x01\x04\x01\xff\x01\x06") # read HW major version (register 1) -# # ser.write(b"\x01\x04\x02\xff\x01\x07") # read HW minor version (register 2) -# # # print(f"In waiting: <{ser.in_waiting}>") -# # -# # data = ser.read(100) -# # print(f"Data: {data}") -# # ser.close() -# # assert not ser.is_open -# # -# # # assert data[0] == '\t' - - -# # FIXME -# # def test_device_events(device: Device) -> None: -# # while True: -# # print(device.event_count()) -# # for msg in device.get_events(): -# # print(msg) -# # time.sleep(0.3) +"""Tests for the device layer: descriptors, to_dataframe, and read_frames.""" + +from typing import ClassVar + +import numpy as np +import pytest + +from harp.protocol._builder import build_message_frame +from harp.protocol._message_type import MessageType +from harp.protocol._payload import PayloadBase, _BitFlag, _GroupMask +from harp.protocol._payload_type import PayloadType + +from harp.device._registers import ( + ClockConfigPayload, + EnableFlag, + OperationControl, + OperationControlPayload, + OperationMode, + ResetDevicePayload, +) +from harp.device.cuttlefish import PinsPayload + + +# --- Minimal fixture payload class ------------------------------------------ + + +class _FlagPayload(PayloadBase[np.uint8]): + _dtype: ClassVar = np.dtype("u1") + _repr_fields: ClassVar = ("flag", "group") + + flag = _BitFlag(0x01) + group = _GroupMask(0x06, 1, OperationMode) + + +# --- _BitFlag behaviour ------------------------------------------------------ + + +def test_bitflag_single_returns_bool_true(): + p = _FlagPayload.from_buffer(bytes([0x01])) + assert p.flag is True + assert type(p.flag) is bool + + +def test_bitflag_single_returns_bool_false(): + p = _FlagPayload.from_buffer(bytes([0x00])) + assert p.flag is False + assert type(p.flag) is bool + + +def test_bitflag_batch_returns_ndarray(): + p = _FlagPayload.from_buffer(bytes([0x01, 0x00, 0x01])) + result = p.flag + assert isinstance(result, np.ndarray) + np.testing.assert_array_equal(result, [True, False, True]) + + +# --- _GroupMask behaviour ---------------------------------------------------- + + +def test_groupmask_single_returns_enum(): + p = _FlagPayload.from_buffer(bytes([0x02])) # bits 1-2 = 01 -> Active + result = p.group + assert result == OperationMode.Active + assert isinstance(result, OperationMode) + + +def test_groupmask_batch_returns_ndarray(): + p = _FlagPayload.from_buffer(bytes([0x00, 0x02, 0x06])) # groups: 0, 1, 3 + result = p.group + assert isinstance(result, np.ndarray) + np.testing.assert_array_equal(result, [0, 1, 3]) + + +# --- OperationControlPayload ------------------------------------------------- + + +def _make_op_ctrl_byte( + mode: OperationMode = OperationMode.Standby, + heartbeat: EnableFlag = EnableFlag.Disabled, +) -> int: + val = int(mode) & 0x03 + val |= (int(heartbeat) & 0x01) << 7 + return val + + +def test_op_ctrl_scalar_from_buffer(): + val = _make_op_ctrl_byte(OperationMode.Active, EnableFlag.Enabled) + p = OperationControlPayload.from_buffer(bytes([val])) + assert p.operation_mode == OperationMode.Active + assert p.heartbeat == EnableFlag.Enabled + assert p.dump_registers is False + + +def test_op_ctrl_init_matches_from_buffer(): + p_init = OperationControlPayload( + operation_mode=OperationMode.Active, heartbeat=EnableFlag.Enabled + ) + val = _make_op_ctrl_byte(OperationMode.Active, EnableFlag.Enabled) + p_buf = OperationControlPayload.from_buffer(bytes([val])) + assert p_init.operation_mode == p_buf.operation_mode + assert p_init.heartbeat == p_buf.heartbeat + + +def test_op_ctrl_batch_descriptor_returns_ndarray(): + vals = [ + _make_op_ctrl_byte(OperationMode.Active, EnableFlag.Enabled), + _make_op_ctrl_byte(OperationMode.Standby, EnableFlag.Disabled), + ] + p = OperationControlPayload.from_buffer(bytes(vals)) + assert isinstance(p.heartbeat, np.ndarray) + np.testing.assert_array_equal(p.heartbeat, [True, False]) + + +def test_op_ctrl_to_dataframe(): + vals = [ + _make_op_ctrl_byte(OperationMode.Active, EnableFlag.Enabled), + _make_op_ctrl_byte(OperationMode.Standby, EnableFlag.Disabled), + ] + p = OperationControlPayload.from_buffer(bytes(vals)) + df = p.to_dataframe() + assert list(df.columns) == list(OperationControlPayload._repr_fields) + assert len(df) == 2 + np.testing.assert_array_equal(df["heartbeat"], [True, False]) + np.testing.assert_array_equal( + df["operation_mode"], + [int(OperationMode.Active), int(OperationMode.Standby)], + ) + + +# --- PinsPayload (cuttlefish) ------------------------------------------------ + + +def test_pins_single_scalar(): + p = PinsPayload.from_buffer(bytes([0b00000101])) + assert p.pin0 is True + assert p.pin1 is False + assert p.pin2 is True + assert p.pin7 is False + + +def test_pins_batch_ndarray(): + p = PinsPayload.from_buffer(bytes([0b00000001, 0b00000010])) + np.testing.assert_array_equal(p.pin0, [True, False]) + np.testing.assert_array_equal(p.pin1, [False, True]) + + +def test_pins_to_dataframe(): + p = PinsPayload.from_buffer(bytes([0b00000101, 0b00000010])) + df = p.to_dataframe() + assert list(df.columns) == list(PinsPayload._repr_fields) + assert len(df) == 2 + + +# --- read_frames round-trip -------------------------------------------------- + + +def _make_frames(values: list, base_time: float = 1.0) -> bytes: + """Build a raw binary buffer of N timestamped OperationControl frames.""" + frames = b"" + for i, v in enumerate(values): + frames += build_message_frame( + MessageType.Read, + address=OperationControl.address, + payload_type=PayloadType.U8, + payload=bytes([v]), + timestamp=base_time + i, + ) + return frames + + +def test_read_frames_count(): + raw = _make_frames([0x01, 0x00, 0x81]) + timestamps, payload = OperationControl.read_frames(raw) + assert len(timestamps) == 3 + assert len(payload) == 3 + + +def test_read_frames_timestamps(): + raw = _make_frames([0x01, 0x00, 0x81], base_time=10.0) + timestamps, _ = OperationControl.read_frames(raw) + np.testing.assert_allclose(timestamps, [10.0, 11.0, 12.0], atol=1e-4) + + +def test_read_frames_payload_type(): + raw = _make_frames([0x01]) + _, payload = OperationControl.read_frames(raw) + assert isinstance(payload, OperationControlPayload) + + +def test_read_frames_bitfield_batch(): + vals = [ + _make_op_ctrl_byte(OperationMode.Active, EnableFlag.Enabled), + _make_op_ctrl_byte(OperationMode.Standby, EnableFlag.Disabled), + ] + raw = _make_frames(vals) + _, payload = OperationControl.read_frames(raw) + np.testing.assert_array_equal(payload.heartbeat, [True, False]) + np.testing.assert_array_equal( + payload.operation_mode, + [int(OperationMode.Active), int(OperationMode.Standby)], + ) + + +def test_read_frames_to_dataframe(): + vals = [_make_op_ctrl_byte(OperationMode.Active), _make_op_ctrl_byte(), _make_op_ctrl_byte()] + raw = _make_frames(vals) + _, payload = OperationControl.read_frames(raw) + df = payload.to_dataframe() + assert len(df) == 3 + assert "heartbeat" in df.columns + assert "operation_mode" in df.columns + + +def test_read_frames_empty(): + timestamps, payload = OperationControl.read_frames(b"") + assert len(timestamps) == 0 + assert len(payload) == 0 \ No newline at end of file From 5040c55ea60e1e744523b72244724c7d08365f76 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sat, 9 May 2026 16:33:11 -0700 Subject: [PATCH 201/267] Linting --- core_registers_example.py | 78 ++++++++++++++----- device_integration.py | 20 ++--- src/harp-device/src/harp/device/_registers.py | 33 ++++---- src/harp-protocol/harp/protocol/_payload.py | 4 +- tests/test_device.py | 9 +-- 5 files changed, 89 insertions(+), 55 deletions(-) diff --git a/core_registers_example.py b/core_registers_example.py index 214041d..00c2b04 100644 --- a/core_registers_example.py +++ b/core_registers_example.py @@ -4,6 +4,8 @@ uv run python core_registers_example.py """ +from pathlib import Path + import numpy as np from harp.device._registers import ( @@ -38,32 +40,66 @@ from harp.protocol._message import HarpMessage examples = [ - (WhoAmI, np.uint16(1216)), - (TimestampSecond, np.uint32(3600)), - (TimestampMicro, np.uint16(500)), - (OperationControl, OperationControlPayload( - operation_mode=OperationMode.Active, - dump_registers=True, - visual_indicators=EnableFlag.Enabled, - operation_led=EnableFlag.Enabled, - heartbeat=EnableFlag.Enabled, - )), - (ResetDevice, ResetDevicePayload(restore_default=True, restore_name=True)), - (DeviceName, DeviceNamePayload("my-harp-device")), - (ClockConfig, ClockConfigPayload(clock_repeater=True, clock_unlock=True)), - (Heartbeat, np.uint16(1)), - (HwVersionH, np.uint8(2)), - (HwVersionL, np.uint8(0)), - (AssemblyVersion, np.uint8(3)), - (CoreVersionH, np.uint8(1)), - (CoreVersionL, np.uint8(4)), + (WhoAmI, np.uint16(1216)), + (TimestampSecond, np.uint32(3600)), + (TimestampMicro, np.uint16(500)), + ( + OperationControl, + OperationControlPayload( + operation_mode=OperationMode.Active, + dump_registers=True, + visual_indicators=EnableFlag.Enabled, + operation_led=EnableFlag.Enabled, + heartbeat=EnableFlag.Enabled, + ), + ), + (ResetDevice, ResetDevicePayload(restore_default=True, restore_name=True)), + (DeviceName, DeviceNamePayload("my-harp-device")), + (ClockConfig, ClockConfigPayload(clock_repeater=True, clock_unlock=True)), + (Heartbeat, np.uint16(1)), + (HwVersionH, np.uint8(2)), + (HwVersionL, np.uint8(0)), + (AssemblyVersion, np.uint8(3)), + (CoreVersionH, np.uint8(1)), + (CoreVersionL, np.uint8(4)), (FirmwareVersionH, np.uint8(2)), (FirmwareVersionL, np.uint8(1)), - (SerialNumber, np.uint16(42)), - (TimestampOffset, np.uint8(0)), + (SerialNumber, np.uint16(42)), + (TimestampOffset, np.uint8(0)), ] +print("=== Live round-trip (format → parse) ===") for register, value in examples: frame = register.format(value) parsed = register.parse(HarpMessage.parse(frame)) print(f"{register.__name__:20s} (addr {register.address:2d}) → {parsed}") + +# --------------------------------------------------------------------------- +# Single-record bitfield access (scalar, no [0] indexing needed) +# --------------------------------------------------------------------------- +print("\n=== Bitfield scalar access ===") +ctrl = OperationControlPayload( + operation_mode=OperationMode.Active, + heartbeat=EnableFlag.Enabled, + visual_indicators=EnableFlag.Enabled, +) +print(f" operation_mode : {ctrl.operation_mode}") # OperationMode.Active +print(f" heartbeat : {ctrl.heartbeat}") # EnableFlag.Enabled +print(f" dump_registers : {ctrl.dump_registers}") # False +print(f" visual_indicators : {ctrl.visual_indicators}") # EnableFlag.Enabled + +# --------------------------------------------------------------------------- +# Bulk read from a .bin file (zero-copy, vectorised) +# --------------------------------------------------------------------------- +BIN_FILE = Path("notes/Behavior.harp/Behavior_10.bin") +print(f"\n=== Bulk read from {BIN_FILE.name} ===") +timestamps, payload = OperationControl.read_frames(BIN_FILE) +print(f" {len(timestamps)} frame(s) read") +print(f" timestamps (s) : {timestamps}") +print(f" operation_mode : {payload.operation_mode}") +print(f" heartbeat : {payload.heartbeat}") + +print("\n DataFrame:") +df = payload.to_dataframe() +df.insert(0, "timestamp", timestamps) +print(df.to_string(index=False)) diff --git a/device_integration.py b/device_integration.py index cc8e9f0..6591426 100644 --- a/device_integration.py +++ b/device_integration.py @@ -38,10 +38,7 @@ ("ResetDevice", ResetDevice), ("SerialNumber", SerialNumber), ("ClockConfig", ClockConfig), - ( - "Heartbeat", - Heartbeat, - ), # TODO This is erroring out in pico devices. May be something with the serial class + ("Heartbeat", Heartbeat), ] @@ -53,12 +50,16 @@ def read_core_registers(dev: Device) -> None: msg = dev.read(reg) print(f"{name:<20} {reg.address} {msg.parsed}") - # TODO just noticed the alias properties inside complex structs payloads like OperationControlPayload - # are returning [value] instead of value. Prob decorate them with something and keep the internal array representation - # hidden internally? + # Bitfield properties return plain Python scalars — no [0] indexing needed + ctrl = dev.read(OperationControl).parsed + print("\n OperationControl breakdown:") + print(f" operation_mode = {ctrl.operation_mode}") + print(f" heartbeat = {ctrl.heartbeat}") + print(f" dump_registers = {ctrl.dump_registers}") + print(f" visual_indicators = {ctrl.visual_indicators}") -def whoa_latency_benchmark(dev: Device, n: int = N_READS) -> None: +def latency_benchmark(dev: Device, n: int = N_READS) -> None: print(f"\n=== WhoAmI round-trip benchmark (n={n:,}) ===") print("Collecting timestamps …") @@ -93,6 +94,7 @@ def whoa_latency_benchmark(dev: Device, n: int = N_READS) -> None: # --------------------------------------------------------------------------- if __name__ == "__main__": + # Live device reads (requires hardware) with Device(PORT) as dev: read_core_registers(dev) - whoa_latency_benchmark(dev) + latency_benchmark(dev) diff --git a/src/harp-device/src/harp/device/_registers.py b/src/harp-device/src/harp/device/_registers.py index 6eec654..e57d000 100644 --- a/src/harp-device/src/harp/device/_registers.py +++ b/src/harp-device/src/harp/device/_registers.py @@ -7,6 +7,7 @@ from harp.protocol._payload_type import PayloadType from harp.protocol._register import RegisterBase, RegisterU8, RegisterU16, RegisterU32 + class OperationMode(enum.IntEnum): """Specifies the operation mode of the device.""" @@ -38,12 +39,12 @@ class OperationControlPayload(PayloadBase[np.uint8]): "heartbeat", ) - operation_mode = _GroupMask(0x03, 0, OperationMode) - dump_registers = _BitFlag(0x08) - mute_replies = _BitFlag(0x10) + operation_mode = _GroupMask(0x03, 0, OperationMode) + dump_registers = _BitFlag(0x08) + mute_replies = _BitFlag(0x10) visual_indicators = _GroupMask(0x20, 5, EnableFlag) - operation_led = _GroupMask(0x40, 6, EnableFlag) - heartbeat = _GroupMask(0x80, 7, EnableFlag) + operation_led = _GroupMask(0x40, 6, EnableFlag) + heartbeat = _GroupMask(0x80, 7, EnableFlag) def __init__( self, @@ -79,13 +80,13 @@ class ResetDevicePayload(PayloadBase[np.uint8]): "boot_from_eeprom", ) - restore_default = _BitFlag(0x01) - restore_eeprom = _BitFlag(0x02) - save = _BitFlag(0x04) - restore_name = _BitFlag(0x08) - update_firmware = _BitFlag(0x20) + restore_default = _BitFlag(0x01) + restore_eeprom = _BitFlag(0x02) + save = _BitFlag(0x04) + restore_name = _BitFlag(0x08) + update_firmware = _BitFlag(0x20) boot_from_default = _BitFlag(0x40) - boot_from_eeprom = _BitFlag(0x80) + boot_from_eeprom = _BitFlag(0x80) def __init__( self, @@ -146,12 +147,12 @@ class ClockConfigPayload(PayloadBase[np.uint8]): "clock_lock", ) - clock_repeater = _BitFlag(0x01) - clock_generator = _BitFlag(0x02) - repeater_capability = _BitFlag(0x08) + clock_repeater = _BitFlag(0x01) + clock_generator = _BitFlag(0x02) + repeater_capability = _BitFlag(0x08) generator_capability = _BitFlag(0x10) - clock_unlock = _BitFlag(0x40) - clock_lock = _BitFlag(0x80) + clock_unlock = _BitFlag(0x40) + clock_lock = _BitFlag(0x80) def __init__( self, diff --git a/src/harp-protocol/harp/protocol/_payload.py b/src/harp-protocol/harp/protocol/_payload.py index 7880794..2090d93 100644 --- a/src/harp-protocol/harp/protocol/_payload.py +++ b/src/harp-protocol/harp/protocol/_payload.py @@ -143,9 +143,7 @@ def to_dataframe(self) -> pd.DataFrame: * Scalar dtypes: a single ``"value"`` column. """ if self._repr_fields is not None: - return pd.DataFrame( - {f: np.atleast_1d(getattr(self, f)) for f in self._repr_fields} - ) + return pd.DataFrame({f: np.atleast_1d(getattr(self, f)) for f in self._repr_fields}) if self._dtype.names is not None: return pd.DataFrame({name: self._arr[name] for name in self._dtype.names}) return pd.DataFrame({"value": self._arr}) diff --git a/tests/test_device.py b/tests/test_device.py index 4b36452..31559a1 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -1,9 +1,8 @@ -"""Tests for the device layer: descriptors, to_dataframe, and read_frames.""" +"""Tests for the device layer: descriptors, to_dataframe, and read_frames.""" from typing import ClassVar import numpy as np -import pytest from harp.protocol._builder import build_message_frame from harp.protocol._message_type import MessageType @@ -11,12 +10,10 @@ from harp.protocol._payload_type import PayloadType from harp.device._registers import ( - ClockConfigPayload, EnableFlag, OperationControl, OperationControlPayload, OperationMode, - ResetDevicePayload, ) from harp.device.cuttlefish import PinsPayload @@ -28,7 +25,7 @@ class _FlagPayload(PayloadBase[np.uint8]): _dtype: ClassVar = np.dtype("u1") _repr_fields: ClassVar = ("flag", "group") - flag = _BitFlag(0x01) + flag = _BitFlag(0x01) group = _GroupMask(0x06, 1, OperationMode) @@ -214,4 +211,4 @@ def test_read_frames_to_dataframe(): def test_read_frames_empty(): timestamps, payload = OperationControl.read_frames(b"") assert len(timestamps) == 0 - assert len(payload) == 0 \ No newline at end of file + assert len(payload) == 0 From 3a282c648362d59af1b9c331d9bfdede81307509 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sat, 9 May 2026 21:17:39 -0700 Subject: [PATCH 202/267] Move scripts and add benchmark --- scripts/_harp_io.py | 148 +++++++++++++++ scripts/benchmark_analog_data.py | 174 ++++++++++++++++++ .../core_registers_example.py | 4 +- .../device_integration.py | 0 4 files changed, 324 insertions(+), 2 deletions(-) create mode 100644 scripts/_harp_io.py create mode 100644 scripts/benchmark_analog_data.py rename core_registers_example.py => scripts/core_registers_example.py (96%) rename device_integration.py => scripts/device_integration.py (100%) diff --git a/scripts/_harp_io.py b/scripts/_harp_io.py new file mode 100644 index 0000000..caaa283 --- /dev/null +++ b/scripts/_harp_io.py @@ -0,0 +1,148 @@ +"""Verbatim copy of harp-python/harp/io.py `read` function. + +Source: https://github.com/harp-tech/harp-python/blob/main/harp/io.py +Vendored here for the benchmark so we don't depend on the harp-python package +being installed. Only `read` is included; `to_file`/`to_buffer` are omitted. + +_BufferLike / _FileLike are inlined from harp/typing.py to keep this file +self-contained. +""" + +import mmap +import sys +from datetime import datetime +from enum import IntEnum +from os import PathLike +from typing import Any, BinaryIO, Optional, Union + +import numpy as np +import numpy.typing as npt +import pandas as pd + +# --------------------------------------------------------------------------- +# Types (inlined from harp/typing.py) +# --------------------------------------------------------------------------- + +if sys.version_info >= (3, 12): + from collections.abc import Buffer as _BufferLike +else: + _BufferLike = Union[bytes, bytearray, memoryview, mmap.mmap, npt.NDArray[Any]] + +_FileLike = Union[str, "PathLike[str]", BinaryIO] + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- + +REFERENCE_EPOCH = datetime(1904, 1, 1) + +_SECONDS_PER_TICK = 32e-6 +_PAYLOAD_TIMESTAMP_MASK = 0x10 + + +class MessageType(IntEnum): + NA = 0 + READ = 1 + WRITE = 2 + EVENT = 3 + + +_messagetypes = [t.name for t in MessageType] + +_dtypefrompayloadtype = { + 1: np.dtype(np.uint8), + 2: np.dtype(np.uint16), + 4: np.dtype(np.uint32), + 8: np.dtype(np.uint64), + 129: np.dtype(np.int8), + 130: np.dtype(np.int16), + 132: np.dtype(np.int32), + 136: np.dtype(np.int64), + 68: np.dtype(np.float32), +} + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- + + +def read( + file_or_buf: Union[_FileLike, _BufferLike], + address: Optional[int] = None, + dtype: Optional[np.dtype] = None, + length: Optional[int] = None, + columns: Optional[list] = None, + epoch: Optional[datetime] = None, + keep_type: bool = False, +) -> pd.DataFrame: + """Read single-register Harp data from the specified file or buffer. + + Returns a pandas DataFrame with a ``Time`` index (seconds as float64, or + datetime if ``epoch`` is given). Column order matches the payload layout. + """ + if isinstance(file_or_buf, (str, PathLike, BinaryIO)) or hasattr(file_or_buf, "readinto"): + data = np.fromfile(file_or_buf, dtype=np.uint8) # type: ignore[arg-type] + else: + data = np.frombuffer(file_or_buf, dtype=np.uint8) # type: ignore[arg-type] + + if len(data) == 0: + return pd.DataFrame( + columns=columns, + index=( + pd.DatetimeIndex([], name="Time") + if epoch + else pd.Index([], dtype=np.float64, name="Time") + ), + ) + + if address is not None and address != data[2]: + raise ValueError(f"expected address {address} but got {data[2]}") + + stride = int(data[1]) + 2 + nrows = len(data) // stride + payloadtype = int(data[4]) + payloadoffset = 5 + index = None + + if payloadtype & _PAYLOAD_TIMESTAMP_MASK: + seconds = np.ndarray( + nrows, dtype=np.uint32, buffer=data, offset=payloadoffset, strides=stride + ) + payloadoffset += 4 + micros = np.ndarray( + nrows, dtype=np.uint16, buffer=data, offset=payloadoffset, strides=stride + ) + payloadoffset += 2 + time = micros * _SECONDS_PER_TICK + seconds + payloadtype = payloadtype & ~_PAYLOAD_TIMESTAMP_MASK + if epoch is not None: + time = epoch + pd.to_timedelta(time, "s") # type: ignore[assignment] + index = pd.DatetimeIndex(time) + index.name = "Time" + else: + index = pd.Series(time) + index.name = "Time" + + payloadsize = stride - payloadoffset - 1 + payload_dtype = _dtypefrompayloadtype[payloadtype] + if dtype is not None and dtype != payload_dtype: + raise ValueError(f"expected payload type {dtype} but got {payload_dtype}") + + elementsize = payload_dtype.itemsize + payloadshape = (nrows, payloadsize // elementsize) + if length is not None and length != payloadshape[1]: + raise ValueError(f"expected payload length {length} but got {payloadshape[1]}") + + payload = np.ndarray( + payloadshape, + dtype=payload_dtype, + buffer=data, + offset=payloadoffset, + strides=(stride, elementsize), + ) + + result = pd.DataFrame(payload, index=index, columns=columns) + if keep_type: + msgtype = np.ndarray(nrows, dtype=np.uint8, buffer=data, offset=0, strides=stride) + result[MessageType.__name__] = pd.Categorical.from_codes(msgtype, categories=_messagetypes) + return result diff --git a/scripts/benchmark_analog_data.py b/scripts/benchmark_analog_data.py new file mode 100644 index 0000000..c91d0d5 --- /dev/null +++ b/scripts/benchmark_analog_data.py @@ -0,0 +1,174 @@ +"""Benchmark: pyharp read_frames vs harp-python read for AnalogData (reg 44). + +AnalogData — address 44, S16 array of length 3, named fields: + analog_input0, encoder, analog_input1 + +The benchmark compares two strategies for decoding ~3.8M frames (~65 MB) of +AnalogData from a Harp binary file into a pandas DataFrame with named columns: + + harp-python strategy: + harp_io.read(file, columns=[...]) → DataFrame directly + + pyharp strategy: + AnalogData.read_frames(file) → (timestamps, payload) + payload.to_dataframe() → DataFrame with named columns + +Both are zero-copy for the payload bytes (strided np.ndarray views into the raw +file buffer). The harp-python path builds the DataFrame in one shot; the pyharp +path splits parsing from presentation. + +Run with: + uv run python scripts/benchmark_analog_data.py +""" + +from __future__ import annotations + +import sys +import timeit +from pathlib import Path +from typing import ClassVar + +import numpy as np + +# --------------------------------------------------------------------------- +# Add repo root to sys.path so this script works whether invoked from root or +# from within scripts/. +# --------------------------------------------------------------------------- +REPO_ROOT = Path(__file__).parent.parent +sys.path.insert(0, str(REPO_ROOT)) + +from scripts._harp_io import read as harp_read # vendored harp-python read() + +from harp.protocol._payload import PayloadBase +from harp.protocol._payload_type import PayloadType +from harp.protocol._register import RegisterBase + +# --------------------------------------------------------------------------- +# AnalogData register definition (hand-written; would come from codegen) +# --------------------------------------------------------------------------- + +ANALOG_COLUMNS = ["analog_input0", "encoder", "analog_input1"] + + +class AnalogDataPayload(PayloadBase[np.void]): + """Payload for AnalogData (register 44): three signed 16-bit channels.""" + + _dtype: ClassVar = np.dtype( + [("analog_input0", " None: + df_pyharp = pyharp_read(BIN_FILE) + df_harp = harp_python_read(BIN_FILE) + + assert len(df_pyharp) == len(df_harp), ( + f"row count mismatch: pyharp={len(df_pyharp)}, harp-python={len(df_harp)}" + ) + for col in ANALOG_COLUMNS: + np.testing.assert_array_equal( + df_pyharp[col].to_numpy(), + df_harp[col].to_numpy(), + err_msg=f"column {col!r} differs", + ) + print(f" Sanity check passed — {len(df_pyharp):,} rows, columns match.") + + +# --------------------------------------------------------------------------- +# Benchmark +# --------------------------------------------------------------------------- + +N_REPEATS = 100 +N_LOOPS = 1 + + +def benchmark(label: str, fn, path: Path) -> np.ndarray: + times = np.array(timeit.repeat(lambda: fn(path), repeat=N_REPEATS, number=N_LOOPS)) + print( + f" {label:<45s} " + f"min={times.min():.3f}s " + f"mean={times.mean():.3f}s " + f"max={times.max():.3f}s " + f"(n={N_REPEATS})" + ) + return times + + +if __name__ == "__main__": + if not BIN_FILE.exists(): + print(f"ERROR: {BIN_FILE} not found. Place the Behavior_44.bin file there.") + sys.exit(1) + + file_mb = BIN_FILE.stat().st_size / 1024**2 + + # Quick probe for frame count + data = np.fromfile(BIN_FILE, dtype=np.uint8) + stride = int(data[1]) + 2 + nrows = len(data) // stride + del data # free before benchmarks + + print("\n=== AnalogData benchmark ===") + print(f" File : {BIN_FILE.name} ({file_mb:.1f} MB)") + print(f" Frames: {nrows:,} | stride={stride} bytes\n") + + print("Sanity check:") + sanity_check() + + print(f"\nBenchmark ({N_REPEATS} repeats, 1 call each):") + t_harp = benchmark("harp-python read()", harp_python_read, BIN_FILE) + t_py_ts = benchmark("pyharp read_frames()+to_dataframe()+ts", pyharp_read, BIN_FILE) + t_py_no_ts = benchmark( + "pyharp read_frames()+to_dataframe() (no ts)", + lambda p: pyharp_read(p, include_timestamp=False), + BIN_FILE, + ) + + print("") + for label, t_py in [("with timestamp", t_py_ts), ("without timestamp", t_py_no_ts)]: + ratio_min = t_py.min() / t_harp.min() + ratio_mean = t_py.mean() / t_harp.mean() + direction = "slower" if ratio_mean > 1 else "faster" + print( + f" pyharp ({label}) vs harp-python :" + f" min={ratio_min:.2f}x mean={ratio_mean:.2f}x" + f" ({direction} by {abs(ratio_mean - 1) * 100:.0f}% on mean)" + ) + + # Show a sample of the result + print("\nSample output (pyharp, first 3 rows):") + df = pyharp_read(BIN_FILE) + print(df.head(3).to_string(index=False)) diff --git a/core_registers_example.py b/scripts/core_registers_example.py similarity index 96% rename from core_registers_example.py rename to scripts/core_registers_example.py index 00c2b04..5fd65a8 100644 --- a/core_registers_example.py +++ b/scripts/core_registers_example.py @@ -1,7 +1,7 @@ """Example: construct and parse every core Harp register payload. Run with: - uv run python core_registers_example.py + uv run python scripts/core_registers_example.py """ from pathlib import Path @@ -91,7 +91,7 @@ # --------------------------------------------------------------------------- # Bulk read from a .bin file (zero-copy, vectorised) # --------------------------------------------------------------------------- -BIN_FILE = Path("notes/Behavior.harp/Behavior_10.bin") +BIN_FILE = Path(__file__).parent.parent / "notes/Behavior.harp/Behavior_10.bin" print(f"\n=== Bulk read from {BIN_FILE.name} ===") timestamps, payload = OperationControl.read_frames(BIN_FILE) print(f" {len(timestamps)} frame(s) read") diff --git a/device_integration.py b/scripts/device_integration.py similarity index 100% rename from device_integration.py rename to scripts/device_integration.py From 9a9832dd1309b9c3e1363282dbd8e512d4da8ecd Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sat, 9 May 2026 21:18:00 -0700 Subject: [PATCH 203/267] Structured arrays can convert directly --- src/harp-protocol/harp/protocol/_payload.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/harp-protocol/harp/protocol/_payload.py b/src/harp-protocol/harp/protocol/_payload.py index 2090d93..3ddd468 100644 --- a/src/harp-protocol/harp/protocol/_payload.py +++ b/src/harp-protocol/harp/protocol/_payload.py @@ -145,7 +145,7 @@ def to_dataframe(self) -> pd.DataFrame: if self._repr_fields is not None: return pd.DataFrame({f: np.atleast_1d(getattr(self, f)) for f in self._repr_fields}) if self._dtype.names is not None: - return pd.DataFrame({name: self._arr[name] for name in self._dtype.names}) + return pd.DataFrame(self._arr) return pd.DataFrame({"value": self._arr}) def __len__(self) -> int: From 1b0b7dc03de10f795cddab4d99a1864e8de11f5a Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sat, 9 May 2026 21:45:27 -0700 Subject: [PATCH 204/267] Pre-compute group-mask enums as a lookup table --- scripts/benchmark_analog_data.py | 26 ++++- src/harp-protocol/harp/protocol/_message.py | 2 - src/harp-protocol/harp/protocol/_payload.py | 43 +++++-- src/harp-protocol/harp/protocol/_register.py | 111 +++++++++++++++---- 4 files changed, 144 insertions(+), 38 deletions(-) diff --git a/scripts/benchmark_analog_data.py b/scripts/benchmark_analog_data.py index c91d0d5..dcf7837 100644 --- a/scripts/benchmark_analog_data.py +++ b/scripts/benchmark_analog_data.py @@ -21,8 +21,6 @@ uv run python scripts/benchmark_analog_data.py """ -from __future__ import annotations - import sys import timeit from pathlib import Path @@ -82,6 +80,11 @@ def pyharp_read(path: Path, *, include_timestamp: bool = True): return df +def pyharp_read_dataframe(path: Path, *, timestamp: bool = True): + """pyharp one-call path: read_dataframe.""" + return AnalogData.read_dataframe(path, timestamp=timestamp) + + def harp_python_read(path: Path): """harp-python path: read() directly to DataFrame.""" return harp_read(path, columns=ANALOG_COLUMNS) @@ -156,14 +159,29 @@ def benchmark(label: str, fn, path: Path) -> np.ndarray: lambda p: pyharp_read(p, include_timestamp=False), BIN_FILE, ) + t_py_rd = benchmark( + "pyharp read_dataframe(timestamp=True)", + pyharp_read_dataframe, + BIN_FILE, + ) + t_py_rd_no_ts = benchmark( + "pyharp read_dataframe(timestamp=False)", + lambda p: pyharp_read_dataframe(p, timestamp=False), + BIN_FILE, + ) print("") - for label, t_py in [("with timestamp", t_py_ts), ("without timestamp", t_py_no_ts)]: + for label, t_py in [ + ("read_frames + to_dataframe + ts", t_py_ts), + ("read_frames + to_dataframe (no ts)", t_py_no_ts), + ("read_dataframe(timestamp=True)", t_py_rd), + ("read_dataframe(timestamp=False)", t_py_rd_no_ts), + ]: ratio_min = t_py.min() / t_harp.min() ratio_mean = t_py.mean() / t_harp.mean() direction = "slower" if ratio_mean > 1 else "faster" print( - f" pyharp ({label}) vs harp-python :" + f" {label:<40s} vs harp-python :" f" min={ratio_min:.2f}x mean={ratio_mean:.2f}x" f" ({direction} by {abs(ratio_mean - 1) * 100:.0f}% on mean)" ) diff --git a/src/harp-protocol/harp/protocol/_message.py b/src/harp-protocol/harp/protocol/_message.py index 70959a0..f499383 100644 --- a/src/harp-protocol/harp/protocol/_message.py +++ b/src/harp-protocol/harp/protocol/_message.py @@ -1,7 +1,5 @@ """Harp message container.""" -from __future__ import annotations - import struct from typing import Any, Generic, TypeVar diff --git a/src/harp-protocol/harp/protocol/_payload.py b/src/harp-protocol/harp/protocol/_payload.py index 3ddd468..8955bb5 100644 --- a/src/harp-protocol/harp/protocol/_payload.py +++ b/src/harp-protocol/harp/protocol/_payload.py @@ -1,10 +1,10 @@ -from __future__ import annotations - -from typing import ClassVar, Generic, Self, TypeVar, final +import inspect +from typing import ClassVar, Generic, TypeVar, final import numpy as np import pandas as pd from numpy.typing import NDArray +from typing_extensions import Self NpStructT = TypeVar("NpStructT", bound=np.generic) @@ -22,7 +22,7 @@ def __init__(self, mask: int) -> None: def __set_name__(self, owner: object, name: str) -> None: self._name = name - def __get__(self, obj: PayloadBase | None, owner: object = None) -> bool | np.ndarray: + def __get__(self, obj: "PayloadBase | None", owner: object = None) -> bool | np.ndarray: if obj is None: return self # type: ignore[return-value] v = (obj._arr & self._mask) != 0 # shape (N,) @@ -40,11 +40,19 @@ def __init__(self, mask: int, shift: int, enum: type) -> None: self._mask = mask self._shift = shift self._enum = enum + # Pre-compute lookup table for vectorised bulk enum decode in to_dataframe. + members = list(enum) + self._categories: list[str] = [m.name for m in members] + max_val = max(int(m) for m in members) + code_dtype = np.int8 if len(members) < 128 else np.int32 + self._code_lookup = np.full(max_val + 1, -1, dtype=code_dtype) + for code, m in enumerate(members): + self._code_lookup[int(m)] = code def __set_name__(self, owner: object, name: str) -> None: self._name = name - def __get__(self, obj: PayloadBase | None, owner: object = None) -> object: + def __get__(self, obj: "PayloadBase | None", owner: object = None) -> object: if obj is None: return self v = (obj._arr & self._mask) >> self._shift # shape (N,) @@ -134,16 +142,35 @@ def raw_payload(self) -> NDArray[NpStructT]: """Raw structured numpy array (alias for ``value``; useful for explicit byte serialisation).""" return self._arr - def to_dataframe(self) -> pd.DataFrame: + def to_dataframe(self, *, decode_enums: bool = True) -> pd.DataFrame: """Convert to a DataFrame. * Bitfield payloads with ``_repr_fields``: one column per descriptor, - shape-polymorphic (bool scalars promoted to length-1 arrays). + shape-polymorphic (bool scalars promoted to length-1 arrays). When + ``decode_enums`` is True, ``_GroupMask`` columns become ordered + ``pd.Categorical`` with the enum member names (typical cost: a few + microseconds per million rows per field). When False, they remain + raw integer arrays. * Structured dtypes: one column per dtype field. * Scalar dtypes: a single ``"value"`` column. """ if self._repr_fields is not None: - return pd.DataFrame({f: np.atleast_1d(getattr(self, f)) for f in self._repr_fields}) + cols: dict[str, object] = {} + cls = type(self) + n = len(self._arr) + for f in self._repr_fields: + desc = inspect.getattr_static(cls, f, None) + if decode_enums and isinstance(desc, _GroupMask): + if n == 1: + m = getattr(self, f) + cols[f] = pd.Categorical([m.name], categories=desc._categories) + else: + raw = (self._arr & desc._mask) >> desc._shift + codes = desc._code_lookup[raw] + cols[f] = pd.Categorical.from_codes(codes, categories=desc._categories) + else: + cols[f] = np.atleast_1d(getattr(self, f)) + return pd.DataFrame(cols) if self._dtype.names is not None: return pd.DataFrame(self._arr) return pd.DataFrame({"value": self._arr}) diff --git a/src/harp-protocol/harp/protocol/_register.py b/src/harp-protocol/harp/protocol/_register.py index 9a57777..eaaaf4f 100644 --- a/src/harp-protocol/harp/protocol/_register.py +++ b/src/harp-protocol/harp/protocol/_register.py @@ -15,6 +15,7 @@ class TimestampSecond(RegisterU32): from typing import Any, ClassVar, Generic, TypeVar, cast, final, overload import numpy as np +import pandas as pd from typing_extensions import Sentinel from ._builder import build_message_frame @@ -79,26 +80,18 @@ def parse(cls, value: HarpMessage | bytes | bytearray | memoryview) -> P: return cast(P, cls.payload_class.from_buffer(buf)) @classmethod - def read_frames( + def _parse_buffer( cls, source: bytes | bytearray | memoryview | Path | str, - ) -> "tuple[np.ndarray, P]": - """Read all frames from a single-register Harp binary buffer or file. - - Parameters - ---------- - source: - Raw bytes, a bytes-like object, or a path to a ``.bin`` file - containing packed Harp frames for a single register. - - Returns - ------- - timestamps : np.ndarray - 1-D float64 array of timestamps in seconds, one per frame. - payload : P - Payload object whose ``_arr`` is a zero-copy strided view into - the raw buffer (shape ``(N,)`` for scalar/bitfield registers, - ``(N, length)`` for array registers). + *, + parse_timestamp: bool = True, + ) -> "tuple[np.ndarray, np.ndarray | None, np.ndarray | None, P]": + """Internal: parse a buffer once, returning (data, timestamps, msgtype_view, payload). + + ``timestamps`` is None when the register is not timestamped **or** + when ``parse_timestamp`` is False (skips the float64 conversion). + ``msgtype_view`` is a strided uint8 view (zero-copy, always computed). + Returns ``data`` so its lifetime anchors the strided views. """ if isinstance(source, (str, Path)): data = np.fromfile(source, dtype=np.uint8) @@ -108,21 +101,22 @@ def read_frames( if len(data) == 0: obj = cls.payload_class.__new__(cls.payload_class) obj._arr = np.empty(0, dtype=cls.payload_class._dtype.base) - return np.empty(0, dtype=np.float64), cast(P, obj) + return data, None, None, cast(P, obj) stride = int(data[1]) + 2 nrows = len(data) // stride is_timestamped = bool(int(data[4]) & 0x10) payload_offset = 11 if is_timestamped else 5 - if is_timestamped: + if is_timestamped and parse_timestamp: ts_s = np.ndarray(nrows, dtype=" "tuple[np.ndarray, P]": + """Read all frames from a single-register Harp binary buffer or file. + + Parameters + ---------- + source: + Raw bytes, a bytes-like object, or a path to a ``.bin`` file + containing packed Harp frames for a single register. + + Returns + ------- + timestamps : np.ndarray + 1-D float64 array of timestamps in seconds, one per frame. + For non-timestamped registers, a synthetic ``arange(N)`` is + returned. + payload : P + Payload object whose ``_arr`` is a zero-copy strided view into + the raw buffer (shape ``(N,)`` for scalar/bitfield registers, + ``(N, length)`` for array registers). + """ + _data, timestamps, _msg, payload = cls._parse_buffer(source, parse_timestamp=True) + if timestamps is None: + timestamps = np.arange(len(payload), dtype=np.float64) + return timestamps, payload + + @classmethod + def read_dataframe( + cls, + source: bytes | bytearray | memoryview | Path | str, + *, + timestamp: bool = True, + message_type: bool = False, + decode_enums: bool = True, + ) -> "pd.DataFrame": + """One-call read: parse all frames into a DataFrame. + + Parameters + ---------- + timestamp: + Insert a ``timestamp`` column (float seconds). For non-timestamped + registers this falls back to a synthetic frame index. + message_type: + Insert a ``message_type`` column as a ``pd.Categorical`` with + categories ``["Read", "Write", "Event"]`` (high bits masked off). + decode_enums: + If True (default), ``_GroupMask`` payload fields are decoded to + ``pd.Categorical`` columns using the enum member names. + Set False for raw integer columns and minimum overhead. + """ + + _data, timestamps, msg_view, payload = cls._parse_buffer(source, parse_timestamp=timestamp) + df = payload.to_dataframe(decode_enums=decode_enums) + if message_type and msg_view is not None: + _msg_names = np.array(["", "Read", "Write", "Event"]) + df.insert( + 0, + "message_type", + pd.Categorical(_msg_names[msg_view & 0x03], categories=_msg_names[1:]), + ) + if timestamp: + if timestamps is None: + timestamps = np.arange(len(payload), dtype=np.float64) + df.insert(0, "timestamp", timestamps) + return df @overload @classmethod From 2aa8235251934cfe7cb24b1c8bd98cd009baf65d Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sun, 10 May 2026 11:16:45 -0700 Subject: [PATCH 205/267] Refactor backend to allow twin classes for Scalar and Bulk types --- src/harp-device/src/harp/device/_registers.py | 50 +-- src/harp-protocol/harp/protocol/_payload.py | 385 +++++++++++++----- src/harp-protocol/harp/protocol/_register.py | 59 +-- tests/protocol/test_register.py | 118 +++++- 4 files changed, 431 insertions(+), 181 deletions(-) diff --git a/src/harp-device/src/harp/device/_registers.py b/src/harp-device/src/harp/device/_registers.py index e57d000..ae33197 100644 --- a/src/harp-device/src/harp/device/_registers.py +++ b/src/harp-device/src/harp/device/_registers.py @@ -30,14 +30,6 @@ class EnableFlag(enum.IntEnum): class OperationControlPayload(PayloadBase[np.uint8]): _dtype: ClassVar = np.dtype("u1") - _repr_fields: ClassVar = ( - "operation_mode", - "dump_registers", - "mute_replies", - "visual_indicators", - "operation_led", - "heartbeat", - ) operation_mode = _GroupMask(0x03, 0, OperationMode) dump_registers = _BitFlag(0x08) @@ -63,22 +55,13 @@ def __init__( val |= (np.uint8(visual_indicators) & np.uint8(0x01)) << np.uint8(5) val |= (np.uint8(operation_led) & np.uint8(0x01)) << np.uint8(6) val |= (np.uint8(heartbeat) & np.uint8(0x01)) << np.uint8(7) - self._arr = np.array([val], dtype=self._dtype) + self._arr = np.array((val,), dtype=self._dtype) class ResetDevicePayload(PayloadBase[np.uint8]): """Payload for the ResetDevice register (address 11).""" _dtype: ClassVar = np.dtype("u1") - _repr_fields: ClassVar = ( - "restore_default", - "restore_eeprom", - "save", - "restore_name", - "update_firmware", - "boot_from_default", - "boot_from_eeprom", - ) restore_default = _BitFlag(0x01) restore_eeprom = _BitFlag(0x02) @@ -107,7 +90,7 @@ def __init__( val |= np.uint8(update_firmware) << np.uint8(5) val |= np.uint8(boot_from_default) << np.uint8(6) val |= np.uint8(boot_from_eeprom) << np.uint8(7) - self._arr = np.array([val], dtype=self._dtype) + self._arr = np.array((val,), dtype=self._dtype) class DeviceNamePayload(PayloadBase[np.uint8]): @@ -117,35 +100,36 @@ class DeviceNamePayload(PayloadBase[np.uint8]): Access the decoded string via ``.name``; ``.value`` returns the raw byte array. """ - _dtype: ClassVar = np.dtype("u1") - _repr_fields: ClassVar = ("name",) _MAX_LEN: ClassVar[int] = 25 + _dtype: ClassVar = np.dtype([("value", "u1", (_MAX_LEN,))]) + _repr_fields: ClassVar = ("name",) def __init__(self, name: str) -> None: encoded = name.encode("ascii")[: self._MAX_LEN] padded = encoded.ljust(self._MAX_LEN, b"\x00") - self._arr = np.frombuffer(padded, dtype=self._dtype).copy() + arr = np.zeros((), dtype=self._dtype) + arr["value"] = np.frombuffer(padded, dtype="u1") + self._arr = arr @property def name(self) -> str: - return self._arr.tobytes().rstrip(b"\x00").decode("ascii") + # 0-D _arr (parse / __init__): _arr["value"] is shape (_MAX_LEN,). + # 1-D _arr (batch): take row 0 — Batch users should iterate rows + # explicitly via _arr["value"] if they need every name. + raw = self._arr["value"] if self._arr.ndim == 0 else self._arr["value"][0] + return raw.tobytes().rstrip(b"\x00").decode("ascii") def to_dataframe(self) -> pd.DataFrame: - return pd.DataFrame({"name": [self.name]}) + if self._arr.ndim == 0: + return pd.DataFrame({"name": [self.name]}) + names = [row.tobytes().rstrip(b"\x00").decode("ascii") for row in self._arr["value"]] + return pd.DataFrame({"name": names}) class ClockConfigPayload(PayloadBase[np.uint8]): """Payload for the ClockConfiguration register (address 14).""" _dtype: ClassVar = np.dtype("u1") - _repr_fields: ClassVar = ( - "clock_repeater", - "clock_generator", - "repeater_capability", - "generator_capability", - "clock_unlock", - "clock_lock", - ) clock_repeater = _BitFlag(0x01) clock_generator = _BitFlag(0x02) @@ -171,7 +155,7 @@ def __init__( val |= np.uint8(generator_capability) << np.uint8(4) val |= np.uint8(clock_unlock) << np.uint8(6) val |= np.uint8(clock_lock) << np.uint8(7) - self._arr = np.array([val], dtype=self._dtype) + self._arr = np.array((val,), dtype=self._dtype) # --------------------------------------------------------------------------- diff --git a/src/harp-protocol/harp/protocol/_payload.py b/src/harp-protocol/harp/protocol/_payload.py index 8955bb5..7ec1e2f 100644 --- a/src/harp-protocol/harp/protocol/_payload.py +++ b/src/harp-protocol/harp/protocol/_payload.py @@ -9,12 +9,35 @@ NpStructT = TypeVar("NpStructT", bound=np.generic) +# --------------------------------------------------------------------------- +# Descriptors +# --------------------------------------------------------------------------- +# Each descriptor comes in a Scalar / Batch pair. The Scalar variant is what +# codegen and hand-written payloads declare; ``PayloadBase.__init_subclass__`` +# auto-derives a ``.Batch`` sibling class with the descriptors swapped to +# their Batch counterparts. Same compute, distinct return-type annotations. +# +# parse(msg) -> payload_class (uses Scalar descriptors → Python/0-D) +# read_frames(buf) -> payload_class.Batch (uses Batch descriptors → ndarray) + + class _BitFlag: - """Single-bit boolean descriptor for bitfield payload classes. + """Single-bit boolean field. Returns ``bool`` from a 0-D record.""" - Returns a plain ``bool`` when the payload holds a single record - (``len(_arr) == 1``), or a boolean ``np.ndarray`` for bulk reads. - """ + def __init__(self, mask: int) -> None: + self._mask = mask + + def __set_name__(self, owner: object, name: str) -> None: + self._name = name + + def __get__(self, obj: "PayloadBase | None", owner: object = None) -> bool: + if obj is None: + return self # type: ignore[return-value] + return bool((obj._arr["value"] & self._mask) != 0) + + +class _BitFlagBatch: + """Batch counterpart of ``_BitFlag``. Returns ``NDArray[bool_]``.""" def __init__(self, mask: int) -> None: self._mask = mask @@ -22,172 +45,328 @@ def __init__(self, mask: int) -> None: def __set_name__(self, owner: object, name: str) -> None: self._name = name - def __get__(self, obj: "PayloadBase | None", owner: object = None) -> bool | np.ndarray: + def __get__(self, obj: "PayloadBase | None", owner: object = None) -> "NDArray[np.bool_]": if obj is None: return self # type: ignore[return-value] - v = (obj._arr & self._mask) != 0 # shape (N,) - return bool(v[0]) if len(v) == 1 else v + return (obj._arr["value"] & self._mask) != 0 + + +def _build_enum_lookup(enum: type) -> "tuple[list[str], np.ndarray]": + """Pre-compute (categories, code_lookup) used by ``to_dataframe`` for a group mask.""" + members = list(enum) + categories = [m.name for m in members] + max_val = max(int(m) for m in members) + code_dtype = np.int8 if len(members) < 128 else np.int32 + code_lookup = np.full(max_val + 1, -1, dtype=code_dtype) + for code, m in enumerate(members): + code_lookup[int(m)] = code + return categories, code_lookup class _GroupMask: - """Multi-bit enum descriptor for bitfield payload classes. + """Multi-bit enum field. Returns the IntEnum member from a 0-D record.""" + + def __init__(self, mask: int, shift: int, enum: type) -> None: + self._mask = mask + self._shift = shift + self._enum = enum + self._categories, self._code_lookup = _build_enum_lookup(enum) + + def __set_name__(self, owner: object, name: str) -> None: + self._name = name + + def __get__(self, obj: "PayloadBase | None", owner: object = None): + if obj is None: + return self + raw = (obj._arr["value"] & self._mask) >> self._shift + return self._enum(int(raw)) - Returns an ``IntEnum`` member when the payload holds a single record, - or a raw integer ``np.ndarray`` for bulk reads. - """ + +class _GroupMaskBatch: + """Batch counterpart of ``_GroupMask``. Returns the raw integer ``NDArray``.""" def __init__(self, mask: int, shift: int, enum: type) -> None: self._mask = mask self._shift = shift self._enum = enum - # Pre-compute lookup table for vectorised bulk enum decode in to_dataframe. - members = list(enum) - self._categories: list[str] = [m.name for m in members] - max_val = max(int(m) for m in members) - code_dtype = np.int8 if len(members) < 128 else np.int32 - self._code_lookup = np.full(max_val + 1, -1, dtype=code_dtype) - for code, m in enumerate(members): - self._code_lookup[int(m)] = code + self._categories, self._code_lookup = _build_enum_lookup(enum) def __set_name__(self, owner: object, name: str) -> None: self._name = name - def __get__(self, obj: "PayloadBase | None", owner: object = None) -> object: + def __get__(self, obj: "PayloadBase | None", owner: object = None) -> np.ndarray: + if obj is None: + return self # type: ignore[return-value] + return (obj._arr["value"] & self._mask) >> self._shift + + +class _Field: + """Struct field accessor. Returns the 0-D numpy scalar for that field.""" + + def __init__(self, field_name: str) -> None: + self._field = field_name + + def __set_name__(self, owner: object, name: str) -> None: + self._name = name + + def __get__(self, obj: "PayloadBase | None", owner: object = None): if obj is None: return self - v = (obj._arr & self._mask) >> self._shift # shape (N,) - return self._enum(int(v[0])) if len(v) == 1 else v + return obj._arr[self._field] -class PayloadBase(Generic[NpStructT]): - """Base class for typed Harp register payloads. +class _FieldBatch: + """Batch counterpart of ``_Field``. Returns the 1-D ``NDArray`` for that field.""" - Subclasses define ``_dtype: ClassVar[np.dtype]`` for the register layout:: + def __init__(self, field_name: str) -> None: + self._field = field_name - class AnalogDataPayload(PayloadBase): - _dtype: ClassVar = np.dtype([ - ("analog_input0", " None: + self._name = name - The type parameter ``NpStructT`` is the numpy scalar type that corresponds - to ``_dtype`` at the type-checker level. For scalar dtypes the two are - equivalent — ``np.dtype(" np.ndarray: + if obj is None: + return self # type: ignore[return-value] + return obj._arr[self._field] + + +# Pairing table used by ``__init_subclass__`` to derive ``.Batch``. +def _swap_to_batch(val: object) -> object | None: + if isinstance(val, _BitFlag): + return _BitFlagBatch(val._mask) + if isinstance(val, _GroupMask): + return _GroupMaskBatch(val._mask, val._shift, val._enum) + if isinstance(val, _Field): + return _FieldBatch(val._field) + return None - class PayloadU32(PayloadBase[np.uint32]): - _dtype: ClassVar = np.dtype(" None: - """Construct a single-sample payload. Structured dtypes use keyword arguments; scalar dtypes use a single positional argument.""" - if self._dtype.names is not None: - # Structured dtype — keyword-only construction + """Construct a single-record payload (``_arr`` is 0-D).""" + names = self._dtype.names + assert names is not None # invariant: __init_subclass__ promotes everything + if names == ("value",): + if len(args) != 1 or kwargs: + raise TypeError(f"{type(self).__name__}() takes exactly one positional argument") + self._arr = np.array((args[0],), dtype=self._dtype) + else: if args: raise TypeError( f"{type(self).__name__}() requires keyword arguments, got positional args" ) - unknown = set(kwargs) - set(self._dtype.names) + unknown = set(kwargs) - set(names) if unknown: raise TypeError(f"{type(self).__name__}() got unexpected kwargs: {sorted(unknown)}") - values = tuple(kwargs[n] for n in self._dtype.names) - self._arr = np.array([values], dtype=self._dtype) - else: - # Scalar dtype — single positional value - if len(args) != 1 or kwargs: - raise TypeError(f"{type(self).__name__}() takes exactly one positional argument") - self._arr = np.array([args[0]], dtype=self._dtype) + values = tuple(kwargs[n] for n in names) + self._arr = np.array(values, dtype=self._dtype) def __init_subclass__(cls, **kwargs: object) -> None: super().__init_subclass__(**kwargs) - # Ensure _dtype is a proper np.dtype when defined on the subclass. + + # 1. Promote _dtype if it was provided as a primitive. if "_dtype" in cls.__dict__: raw = cls.__dict__["_dtype"] if not isinstance(raw, np.dtype): - cls._dtype = np.dtype(raw) + raw = np.dtype(raw) + if raw.names is None: + raw = np.dtype([("value", raw)]) + cls._dtype = raw + + # 2. Skip auto-derived Batch siblings so we don't recurse / re-pair. + if cls.__dict__.get("_is_batch_view", False): + return + + # 3. Multi-field struct payloads: auto-generate _Field accessors for + # any name that the user hasn't already declared. + has_bitfield = any(isinstance(val, _SCALAR_BITFIELD_TYPES) for val in vars(cls).values()) + names = cls._dtype.names + if not has_bitfield and names is not None and names != ("value",): + for name in names: + if name not in vars(cls): + setattr(cls, name, _Field(name)) + + # 4. Auto-derive _repr_fields if not explicitly set on this class. + if "_repr_fields" not in cls.__dict__: + bitfield_names = tuple( + name for name, val in vars(cls).items() if isinstance(val, _SCALAR_BITFIELD_TYPES) + ) + if bitfield_names: + cls._repr_fields = bitfield_names + elif names is not None and names != ("value",): + cls._repr_fields = names + else: + cls._repr_fields = ("value",) + + # 5. Synthesise the .Batch sibling by swapping each declared + # descriptor for its batch counterpart. If nothing to swap, the + # class is its own Batch (e.g. plain scalar PayloadU8). + batch_attrs: dict[str, object] = {"_is_batch_view": True} + for name, val in vars(cls).items(): + swapped = _swap_to_batch(val) + if swapped is not None: + batch_attrs[name] = swapped + + if len(batch_attrs) > 1: + cls.Batch = type(f"{cls.__name__}Batch", (cls,), batch_attrs) + else: + cls.Batch = cls + + # ------------------------------------------------------------------ + # Constructors + # ------------------------------------------------------------------ @classmethod - def from_buffer(cls, buf: bytes | bytearray | memoryview) -> Self: - """Construct from a raw byte buffer interpreted as an array of ``_dtype`` records.""" - arr = np.frombuffer(buf, dtype=cls._dtype) + def from_array(cls, arr: "np.ndarray") -> Self: + """Wrap a pre-built numpy array of ``_dtype`` records. + + The shape of ``arr`` determines the storage mode: 0-D = single record + (used by ``parse``), 1-D = batch (used by ``read_frames``). For batch + construction in tests, call this on the ``.Batch`` sibling class. + """ obj = cls.__new__(cls) obj._arr = arr return obj + @classmethod + def from_buffer(cls, buf: bytes | bytearray | memoryview) -> Self: + """Decode a packed byte buffer into a batch payload (``_arr.ndim == 1``). + + Always returns an instance of ``cls.Batch`` so descriptor reads + produce ``ndarray`` columns regardless of the calling class. Use + :py:meth:`RegisterBase.parse` for the single-record / scalar path. + """ + arr = np.frombuffer(buf, dtype=cls._dtype) + batch_cls = cls.Batch + obj = batch_cls.__new__(batch_cls) + obj._arr = arr + return obj # type: ignore[return-value] + + # ------------------------------------------------------------------ + # Value accessors + # ------------------------------------------------------------------ + @property - def value(self) -> NDArray[NpStructT]: - """Returns the backing array of ``_dtype`` records.""" - return self._arr + def value(self) -> "NDArray[NpStructT]": + """Return the underlying value(s). + + For single-field ``("value",)`` payloads: + * 0-D _arr → 0-D numpy scalar (``np.uint16(1216)``) for primitive + registers, or 1-D sub-array for fixed-length array registers. + * 1-D _arr → 1-D ndarray (or 2-D for array registers). + + For multi-field structured payloads, returns the structured ``_arr`` + unchanged. + """ + arr = self._arr + if arr.dtype.names == ("value",): + return arr["value"] + return arr @property def raw_payload(self) -> NDArray[NpStructT]: - """Raw structured numpy array (alias for ``value``; useful for explicit byte serialisation).""" + """The backing ``_arr``. ``.tobytes()`` produces the wire payload.""" return self._arr + # ------------------------------------------------------------------ + # DataFrame + # ------------------------------------------------------------------ + def to_dataframe(self, *, decode_enums: bool = True) -> pd.DataFrame: - """Convert to a DataFrame. - - * Bitfield payloads with ``_repr_fields``: one column per descriptor, - shape-polymorphic (bool scalars promoted to length-1 arrays). When - ``decode_enums`` is True, ``_GroupMask`` columns become ordered - ``pd.Categorical`` with the enum member names (typical cost: a few - microseconds per million rows per field). When False, they remain - raw integer arrays. - * Structured dtypes: one column per dtype field. - * Scalar dtypes: a single ``"value"`` column. - """ - if self._repr_fields is not None: + """Convert to a DataFrame. Works on both 0-D (1 row) and 1-D (N rows) ``_arr``.""" + arr = np.atleast_1d(self._arr) + cls = type(self) + repr_fields = self._repr_fields + + # Bitfield path — at least one repr field is a bit/group descriptor. + has_bitfield = any( + isinstance( + inspect.getattr_static(cls, f, None), _SCALAR_BITFIELD_TYPES + _BATCH_BITFIELD_TYPES + ) + for f in repr_fields + ) + if has_bitfield: cols: dict[str, object] = {} - cls = type(self) - n = len(self._arr) - for f in self._repr_fields: + value_col = arr["value"] + for f in repr_fields: desc = inspect.getattr_static(cls, f, None) - if decode_enums and isinstance(desc, _GroupMask): - if n == 1: - m = getattr(self, f) - cols[f] = pd.Categorical([m.name], categories=desc._categories) - else: - raw = (self._arr & desc._mask) >> desc._shift + if isinstance(desc, _GROUP_TYPES): + raw = (value_col & desc._mask) >> desc._shift + if decode_enums: codes = desc._code_lookup[raw] cols[f] = pd.Categorical.from_codes(codes, categories=desc._categories) + else: + cols[f] = raw + elif isinstance(desc, _FLAG_TYPES): + cols[f] = (value_col & desc._mask) != 0 else: + # Custom property / hand-written field. Lift via atleast_1d + # so 0-D scalars become 1-row columns. cols[f] = np.atleast_1d(getattr(self, f)) return pd.DataFrame(cols) - if self._dtype.names is not None: - return pd.DataFrame(self._arr) - return pd.DataFrame({"value": self._arr}) + + # Structured path — walk dtype fields, expand sub-arrays. + cols = {} + names = self._dtype.names + single_value_field = names == ("value",) + for name in names: + field_dtype, _ = self._dtype.fields[name] + sub = arr[name] + if field_dtype.subdtype is None: + cols[name] = sub + else: + _, subshape = field_dtype.subdtype + count = int(np.prod(subshape)) + flat = sub.reshape(len(arr), count) + for i in range(count): + col = str(i) if single_value_field else f"{name}_{i}" + cols[col] = flat[:, i] + return pd.DataFrame(cols) + + # ------------------------------------------------------------------ + # Dunder + # ------------------------------------------------------------------ def __len__(self) -> int: - return len(self._arr) + return 1 if self._arr.ndim == 0 else len(self._arr) def _repr_kwargs(self) -> str: - """Return the ``key=value`` portion used by ``__repr__`` and ``__str__``.""" - fields: tuple[str, ...] - if self._repr_fields is not None: - fields = self._repr_fields - elif self._dtype.names is not None: - fields = self._dtype.names - else: - return f"value={self.value!r}" - return ", ".join(f"{f}={getattr(self, f)!r}" for f in fields) + return ", ".join(f"{f}={getattr(self, f)!r}" for f in self._repr_fields) def __repr__(self) -> str: return f"{type(self).__name__}({self._repr_kwargs()})" @@ -241,8 +420,8 @@ class PayloadFloat(PayloadBase[np.float32]): # ------------------------------------------------------------------ # Array payload classes — one per PayloadType. # Each message payload is a fixed-length array of the element type. -# Length is not stored on the class; pass it explicitly to -# ``from_buffer_with_length()``. +# Length is not stored on the class; pass it explicitly to the +# array-register factory (RegisterU16Array(addr, length=N)). # ------------------------------------------------------------------ diff --git a/src/harp-protocol/harp/protocol/_register.py b/src/harp-protocol/harp/protocol/_register.py index eaaaf4f..2ec6307 100644 --- a/src/harp-protocol/harp/protocol/_register.py +++ b/src/harp-protocol/harp/protocol/_register.py @@ -75,9 +75,16 @@ class RegisterBase(ABC, Generic[P]): @classmethod def parse(cls, value: HarpMessage | bytes | bytearray | memoryview) -> P: - """Parse the payload from a ``HarpMessage`` or raw bytes.""" + """Parse a single message into a scalar-typed payload (``_arr.ndim == 0``). + + Reads exactly one record's worth of bytes via ``np.frombuffer(..., + count=1)[0]``; descriptors on the resulting payload return Python / + 0-D scalars (``int``, ``bool``, ``IntEnum``, etc.). Use + :py:meth:`read_frames` for bulk file/buffer reads. + """ buf = value.payload if isinstance(value, HarpMessage) else value - return cast(P, cls.payload_class.from_buffer(buf)) + record = np.frombuffer(buf, dtype=cls.payload_class._dtype, count=1)[0] + return cast(P, cls.payload_class.from_array(record)) @classmethod def _parse_buffer( @@ -88,20 +95,21 @@ def _parse_buffer( ) -> "tuple[np.ndarray, np.ndarray | None, np.ndarray | None, P]": """Internal: parse a buffer once, returning (data, timestamps, msgtype_view, payload). - ``timestamps`` is None when the register is not timestamped **or** - when ``parse_timestamp`` is False (skips the float64 conversion). - ``msgtype_view`` is a strided uint8 view (zero-copy, always computed). - Returns ``data`` so its lifetime anchors the strided views. + Always wraps the result in ``payload_class.Batch`` (ndarray-typed + accessors). ``timestamps`` is None when the register is not + timestamped or when ``parse_timestamp`` is False. ``msgtype_view`` + is a strided uint8 view (zero-copy, always computed). Returns + ``data`` so its lifetime anchors the strided views. """ + batch_cls = cls.payload_class.Batch if isinstance(source, (str, Path)): data = np.fromfile(source, dtype=np.uint8) else: data = np.frombuffer(source, dtype=np.uint8) if len(data) == 0: - obj = cls.payload_class.__new__(cls.payload_class) - obj._arr = np.empty(0, dtype=cls.payload_class._dtype.base) - return data, None, None, cast(P, obj) + payload = batch_cls.from_array(np.empty(0, dtype=batch_cls._dtype)) + return data, None, None, cast(P, payload) stride = int(data[1]) + 2 nrows = len(data) // stride @@ -117,22 +125,20 @@ def _parse_buffer( msgtype_view = np.ndarray(nrows, dtype=np.uint8, buffer=data, offset=0, strides=stride) - elem_dtype = cls.payload_class._dtype.base - elem_size = elem_dtype.itemsize - length = cls.length or 1 + # Single zero-copy strided view: one structured record per frame. + # Sub-array fields (array registers) and multi-field payloads are + # both byte-packed in the file, so the structured dtype itemsize + # equals the payload byte length. payload_arr = np.ndarray( - (nrows, length), - dtype=elem_dtype, + nrows, + dtype=batch_cls._dtype, buffer=data, offset=payload_offset, - strides=(stride, elem_size), + strides=stride, ) - if length == 1: - payload_arr = payload_arr[:, 0] - obj = cls.payload_class.__new__(cls.payload_class) - obj._arr = payload_arr - return data, timestamps, msgtype_view, cast(P, obj) + payload = batch_cls.from_array(payload_arr) + return data, timestamps, msgtype_view, cast(P, payload) @classmethod def read_frames( @@ -154,9 +160,10 @@ def read_frames( For non-timestamped registers, a synthetic ``arange(N)`` is returned. payload : P - Payload object whose ``_arr`` is a zero-copy strided view into - the raw buffer (shape ``(N,)`` for scalar/bitfield registers, - ``(N, length)`` for array registers). + Instance of ``payload_class.Batch`` whose ``_arr`` is a + zero-copy strided view into the raw buffer (shape ``(N,)`` for + scalar/bitfield registers, ``(N, length)`` for array + registers). Descriptor reads return ``ndarray`` columns. """ _data, timestamps, _msg, payload = cls._parse_buffer(source, parse_timestamp=True) if timestamps is None: @@ -306,10 +313,14 @@ class _ArrayRegisterMeta(ABCMeta): def __call__(cls: "type[_AR]", address: int, *, length: int) -> "type[_AR]": # type: ignore[override, misc] base_payload = cls.payload_class # type: ignore[attr-defined] + # ``base_payload._dtype`` is the auto-promoted single-field structured + # dtype ``[("value", primitive)]``; extract the primitive and rebuild + # with a sub-array field of the requested length. + inner = base_payload._dtype.fields["value"][0] concrete_payload = type( f"{base_payload.__name__}_{length}", (base_payload,), - {"_dtype": np.dtype((base_payload._dtype, length))}, # type: ignore[attr-defined] + {"_dtype": np.dtype([("value", inner, (length,))])}, ) return cast( "type[_AR]", diff --git a/tests/protocol/test_register.py b/tests/protocol/test_register.py index 0de47ce..694c75d 100644 --- a/tests/protocol/test_register.py +++ b/tests/protocol/test_register.py @@ -137,7 +137,9 @@ def test_named_register_roundtrip(value): msg = _parse_frame(frame) parsed = TimestampSecond.parse(msg) assert isinstance(parsed, PayloadU32) - assert parsed.value == np.array([value]) + # parse() returns a 0-D scalar; compare to the Python value directly. + assert parsed.value == value + assert parsed.value.ndim == 0 def test_named_register_format_read_frame(): @@ -159,7 +161,8 @@ def test_factory_format_and_parse(): msg = _parse_frame(frame) parsed = reg.parse(msg) assert isinstance(parsed, PayloadU32) - assert parsed.value == np.array([100]) + assert parsed.value == 100 + assert parsed.value.ndim == 0 def test_factory_different_addresses_are_independent(): @@ -195,7 +198,7 @@ def test_format_with_payload_instance_via_register(): frame = TimestampSecond.format(payload) msg = _parse_frame(frame) parsed = TimestampSecond.parse(msg) - assert parsed.value == np.array([42]) + assert parsed.value == 42 def test_structured_register_format_single_sample(): @@ -204,9 +207,10 @@ def test_structured_register_format_single_sample(): msg = _parse_frame(frame) parsed = AnalogData.parse(msg) assert isinstance(parsed, AnalogDataPayload) - assert int(parsed.analog_input0[0]) == 100 - assert int(parsed.encoder[0]) == 512 - assert int(parsed.analog_input1[0]) == -200 + # parse() yields a 0-D record; @property accessors return numpy scalars. + assert int(parsed.analog_input0) == 100 + assert int(parsed.encoder) == 512 + assert int(parsed.analog_input1) == -200 def test_structured_register_to_dataframe(): @@ -214,7 +218,8 @@ def test_structured_register_to_dataframe(): [(1, 2, 3), (4, 5, 6)], dtype=AnalogDataPayload._dtype, ).tobytes() - bulk = AnalogData.parse(raw) + # Bulk decode goes through .Batch; from_buffer handles the redirect. + bulk = AnalogDataPayload.from_buffer(raw) df = bulk.to_dataframe() assert list(df.columns) == ["analog_input0", "encoder", "analog_input1"] assert len(df) == 2 @@ -251,9 +256,9 @@ def test_array_register_parse_roundtrip(): frame = reg.format(values) msg = _parse_frame(frame) parsed = reg.parse(msg) - # The payload array contains the packed sub-array as a single element - flat = parsed.raw_payload.flatten().view(np.dtype(" len == 1 => .value returns arr[0], shape (3,) assert len(parsed) == 1 v = parsed.value assert isinstance(v, np.ndarray) - assert v.shape == (1, 3) - np.testing.assert_array_equal(v, np.array([values])) + assert v.shape == (3,) + np.testing.assert_array_equal(v, values) def test_array_register_value_multi(): - """.value on a multi-record array-register payload returns the full 2-D array.""" + """.value on a Batch payload for an array register: 2-D (N, length) ndarray.""" reg = RegisterU32Array(0x08, length=3) - # Two rows of 3 elements each; pass as flat bytes rows = np.array([[10, 20, 30], [40, 50, 60]], dtype=np.dtype(" len == 2 => .value returns the full array + # Bulk decode → goes through .Batch (1-D _arr of length 2). + bulk = reg.payload_class.from_buffer(rows.tobytes()) assert len(bulk) == 2 v = bulk.value assert isinstance(v, np.ndarray) assert v.shape == (2, 3) np.testing.assert_array_equal(v[0], [10, 20, 30]) np.testing.assert_array_equal(v[1], [40, 50, 60]) + + +# --------------------------------------------------------------------------- +# 10. parse vs read_frames / .Batch contract +# --------------------------------------------------------------------------- + + +def test_parse_returns_zero_dim_arr(): + """parse() always wraps a single record in a 0-D _arr.""" + frame = TimestampSecond.format(42) + msg = _parse_frame(frame) + parsed = TimestampSecond.parse(msg) + assert parsed._arr.ndim == 0 + assert isinstance(parsed, PayloadU32) + # parse() returns the scalar payload class, not the Batch sibling. + assert type(parsed) is PayloadU32 + + +def test_parse_does_not_overrun_buffer(): + """parse() reads exactly one record even if the buffer is larger.""" + # Two-record buffer in raw form (no Harp header — exercise PayloadBase path). + raw = np.array([42, 99], dtype=np.dtype(" Date: Sun, 10 May 2026 21:14:18 -0700 Subject: [PATCH 206/267] Simplify dtype logic but procedurally generating type from fields --- scripts/benchmark_analog_data.py | 8 +- src/harp-device/src/harp/device/_registers.py | 118 ++-- src/harp-protocol/harp/protocol/_payload.py | 503 ++++++++---------- src/harp-protocol/harp/protocol/_register.py | 86 +-- tests/protocol/test_converter.py | 215 ++++++++ tests/protocol/test_payload.py | 18 +- tests/protocol/test_register.py | 64 +-- 7 files changed, 530 insertions(+), 482 deletions(-) create mode 100644 tests/protocol/test_converter.py diff --git a/scripts/benchmark_analog_data.py b/scripts/benchmark_analog_data.py index dcf7837..fda3026 100644 --- a/scripts/benchmark_analog_data.py +++ b/scripts/benchmark_analog_data.py @@ -37,7 +37,7 @@ from scripts._harp_io import read as harp_read # vendored harp-python read() -from harp.protocol._payload import PayloadBase +from harp.protocol._payload import PayloadBase, _Field, _IdentityConverter from harp.protocol._payload_type import PayloadType from harp.protocol._register import RegisterBase @@ -51,9 +51,9 @@ class AnalogDataPayload(PayloadBase[np.void]): """Payload for AnalogData (register 44): three signed 16-bit channels.""" - _dtype: ClassVar = np.dtype( - [("analog_input0", " None: - val = np.uint8(0) - val |= np.uint8(operation_mode) & np.uint8(0x03) - val |= np.uint8(dump_registers) << np.uint8(3) - val |= np.uint8(mute_replies) << np.uint8(4) - val |= (np.uint8(visual_indicators) & np.uint8(0x01)) << np.uint8(5) - val |= (np.uint8(operation_led) & np.uint8(0x01)) << np.uint8(6) - val |= (np.uint8(heartbeat) & np.uint8(0x01)) << np.uint8(7) - self._arr = np.array((val,), dtype=self._dtype) + operation_mode = _GroupMask(0x03, 0, OperationMode, dtype=np.uint8) + dump_registers = _BitFlag(0x08, dtype=np.uint8) + mute_replies = _BitFlag(0x10, dtype=np.uint8) + visual_indicators = _GroupMask(0x20, 5, EnableFlag, dtype=np.uint8) + operation_led = _GroupMask(0x40, 6, EnableFlag, dtype=np.uint8) + heartbeat = _GroupMask(0x80, 7, EnableFlag, dtype=np.uint8) class ResetDevicePayload(PayloadBase[np.uint8]): """Payload for the ResetDevice register (address 11).""" - _dtype: ClassVar = np.dtype("u1") - - restore_default = _BitFlag(0x01) - restore_eeprom = _BitFlag(0x02) - save = _BitFlag(0x04) - restore_name = _BitFlag(0x08) - update_firmware = _BitFlag(0x20) - boot_from_default = _BitFlag(0x40) - boot_from_eeprom = _BitFlag(0x80) - - def __init__( - self, - *, - restore_default: bool = False, - restore_eeprom: bool = False, - save: bool = False, - restore_name: bool = False, - update_firmware: bool = False, - boot_from_default: bool = False, - boot_from_eeprom: bool = False, - ) -> None: - val = np.uint8(0) - val |= np.uint8(restore_default) << np.uint8(0) - val |= np.uint8(restore_eeprom) << np.uint8(1) - val |= np.uint8(save) << np.uint8(2) - val |= np.uint8(restore_name) << np.uint8(3) - val |= np.uint8(update_firmware) << np.uint8(5) - val |= np.uint8(boot_from_default) << np.uint8(6) - val |= np.uint8(boot_from_eeprom) << np.uint8(7) - self._arr = np.array((val,), dtype=self._dtype) + restore_default = _BitFlag(0x01, dtype=np.uint8) + restore_eeprom = _BitFlag(0x02, dtype=np.uint8) + save = _BitFlag(0x04, dtype=np.uint8) + restore_name = _BitFlag(0x08, dtype=np.uint8) + update_firmware = _BitFlag(0x20, dtype=np.uint8) + boot_from_default = _BitFlag(0x40, dtype=np.uint8) + boot_from_eeprom = _BitFlag(0x80, dtype=np.uint8) class DeviceNamePayload(PayloadBase[np.uint8]): """Payload for the DeviceName register (address 12). Stores a user-specified ASCII device name padded to 25 bytes. - Access the decoded string via ``.name``; ``.value`` returns the raw byte array. + Access the decoded string via ``.name``; ``.value`` (inherited) returns + the raw byte sub-array. """ _MAX_LEN: ClassVar[int] = 25 - _dtype: ClassVar = np.dtype([("value", "u1", (_MAX_LEN,))]) + + value = _Field(_IdentityConverter(np.dtype((np.uint8, (_MAX_LEN,))))) + _repr_fields: ClassVar = ("name",) def __init__(self, name: str) -> None: @@ -129,33 +94,12 @@ def to_dataframe(self) -> pd.DataFrame: class ClockConfigPayload(PayloadBase[np.uint8]): """Payload for the ClockConfiguration register (address 14).""" - _dtype: ClassVar = np.dtype("u1") - - clock_repeater = _BitFlag(0x01) - clock_generator = _BitFlag(0x02) - repeater_capability = _BitFlag(0x08) - generator_capability = _BitFlag(0x10) - clock_unlock = _BitFlag(0x40) - clock_lock = _BitFlag(0x80) - - def __init__( - self, - *, - clock_repeater: bool = False, - clock_generator: bool = False, - repeater_capability: bool = False, - generator_capability: bool = False, - clock_unlock: bool = False, - clock_lock: bool = False, - ) -> None: - val = np.uint8(0) - val |= np.uint8(clock_repeater) << np.uint8(0) - val |= np.uint8(clock_generator) << np.uint8(1) - val |= np.uint8(repeater_capability) << np.uint8(3) - val |= np.uint8(generator_capability) << np.uint8(4) - val |= np.uint8(clock_unlock) << np.uint8(6) - val |= np.uint8(clock_lock) << np.uint8(7) - self._arr = np.array((val,), dtype=self._dtype) + clock_repeater = _BitFlag(0x01, dtype=np.uint8) + clock_generator = _BitFlag(0x02, dtype=np.uint8) + repeater_capability = _BitFlag(0x08, dtype=np.uint8) + generator_capability = _BitFlag(0x10, dtype=np.uint8) + clock_unlock = _BitFlag(0x40, dtype=np.uint8) + clock_lock = _BitFlag(0x80, dtype=np.uint8) # --------------------------------------------------------------------------- diff --git a/src/harp-protocol/harp/protocol/_payload.py b/src/harp-protocol/harp/protocol/_payload.py index 7ec1e2f..20a95a6 100644 --- a/src/harp-protocol/harp/protocol/_payload.py +++ b/src/harp-protocol/harp/protocol/_payload.py @@ -1,5 +1,5 @@ -import inspect -from typing import ClassVar, Generic, TypeVar, final +from abc import ABC, abstractmethod +from typing import Any, ClassVar, Generic, TypeVar, final import numpy as np import pandas as pd @@ -9,50 +9,101 @@ NpStructT = TypeVar("NpStructT", bound=np.generic) -# --------------------------------------------------------------------------- -# Descriptors -# --------------------------------------------------------------------------- -# Each descriptor comes in a Scalar / Batch pair. The Scalar variant is what -# codegen and hand-written payloads declare; ``PayloadBase.__init_subclass__`` -# auto-derives a ``.Batch`` sibling class with the descriptors swapped to -# their Batch counterparts. Same compute, distinct return-type annotations. -# -# parse(msg) -> payload_class (uses Scalar descriptors → Python/0-D) -# read_frames(buf) -> payload_class.Batch (uses Batch descriptors → ndarray) +class _Converter(ABC): + dtype: np.dtype + python_type: type + @abstractmethod + def decode_scalar(self, view: Any) -> Any: ... -class _BitFlag: - """Single-bit boolean field. Returns ``bool`` from a 0-D record.""" + @abstractmethod + def decode_batch(self, view: Any) -> Any: ... - def __init__(self, mask: int) -> None: - self._mask = mask + @abstractmethod + def encode_into(self, view: Any, value: Any) -> None: ... - def __set_name__(self, owner: object, name: str) -> None: - self._name = name - def __get__(self, obj: "PayloadBase | None", owner: object = None) -> bool: - if obj is None: - return self # type: ignore[return-value] - return bool((obj._arr["value"] & self._mask) != 0) +class _IdentityConverter(_Converter): + def __init__(self, dtype: "np.dtype | str | type") -> None: + self.dtype = np.dtype(dtype) + self.python_type = self.dtype.type + def decode_scalar(self, view: Any) -> Any: + return view -class _BitFlagBatch: - """Batch counterpart of ``_BitFlag``. Returns ``NDArray[bool_]``.""" + def decode_batch(self, view: Any) -> Any: + return view - def __init__(self, mask: int) -> None: - self._mask = mask + def encode_into(self, view: Any, value: Any) -> None: + view[...] = value - def __set_name__(self, owner: object, name: str) -> None: + +class _StringConverter(_Converter): + python_type = str + + def __init__(self, length: int, encoding: str = "ascii") -> None: + self._length = length + self._encoding = encoding + self.dtype = np.dtype((np.uint8, (length,))) + + def decode_scalar(self, view: Any) -> str: + return bytes(view).rstrip(b"\x00").decode(self._encoding) + + def decode_batch(self, view: Any) -> Any: + return np.array( + [bytes(row).rstrip(b"\x00").decode(self._encoding) for row in view], + dtype=object, + ) + + def encode_into(self, view: Any, value: str) -> None: + encoded = value.encode(self._encoding)[: self._length] + padded = encoded.ljust(self._length, b"\x00") + view[...] = np.frombuffer(padded, dtype=np.uint8) + + +class _Field: + def __init__(self, converter: _Converter, *, name: str | None = None) -> None: + self._converter = converter self._name = name - def __get__(self, obj: "PayloadBase | None", owner: object = None) -> "NDArray[np.bool_]": + def __set_name__(self, owner: object, name: str) -> None: + if self._name is None: + self._name = name + + def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: + if obj is None: + return self + view = obj._arr[self._name] + if obj._arr.ndim == 0: + return self._converter.decode_scalar(view) + return self._converter.decode_batch(view) + + +class _BitFlag: + def __init__( + self, + mask: int, + *, + slot: str = "value", + dtype: "np.dtype | str | type" = np.uint8, + ) -> None: + self._mask = mask + self._slot = slot + self._dtype = np.dtype(dtype) + + def __get__( + self, obj: "PayloadBase | None", owner: object = None + ) -> "bool | NDArray[np.bool_]": if obj is None: return self # type: ignore[return-value] - return (obj._arr["value"] & self._mask) != 0 + view = obj._arr[self._slot] + result = (view & self._mask) != 0 + if obj._arr.ndim == 0: + return bool(result) + return result def _build_enum_lookup(enum: type) -> "tuple[list[str], np.ndarray]": - """Pre-compute (categories, code_lookup) used by ``to_dataframe`` for a group mask.""" members = list(enum) categories = [m.name for m in members] max_val = max(int(m) for m in members) @@ -64,235 +115,167 @@ def _build_enum_lookup(enum: type) -> "tuple[list[str], np.ndarray]": class _GroupMask: - """Multi-bit enum field. Returns the IntEnum member from a 0-D record.""" - - def __init__(self, mask: int, shift: int, enum: type) -> None: + def __init__( + self, + mask: int, + shift: int, + enum: type, + *, + slot: str = "value", + dtype: "np.dtype | str | type" = np.uint8, + ) -> None: self._mask = mask self._shift = shift self._enum = enum + self._slot = slot + self._dtype = np.dtype(dtype) self._categories, self._code_lookup = _build_enum_lookup(enum) - def __set_name__(self, owner: object, name: str) -> None: - self._name = name - - def __get__(self, obj: "PayloadBase | None", owner: object = None): - if obj is None: - return self - raw = (obj._arr["value"] & self._mask) >> self._shift - return self._enum(int(raw)) - - -class _GroupMaskBatch: - """Batch counterpart of ``_GroupMask``. Returns the raw integer ``NDArray``.""" - - def __init__(self, mask: int, shift: int, enum: type) -> None: - self._mask = mask - self._shift = shift - self._enum = enum - self._categories, self._code_lookup = _build_enum_lookup(enum) - - def __set_name__(self, owner: object, name: str) -> None: - self._name = name - - def __get__(self, obj: "PayloadBase | None", owner: object = None) -> np.ndarray: - if obj is None: - return self # type: ignore[return-value] - return (obj._arr["value"] & self._mask) >> self._shift - - -class _Field: - """Struct field accessor. Returns the 0-D numpy scalar for that field.""" - - def __init__(self, field_name: str) -> None: - self._field = field_name - - def __set_name__(self, owner: object, name: str) -> None: - self._name = name - def __get__(self, obj: "PayloadBase | None", owner: object = None): if obj is None: return self - return obj._arr[self._field] + view = obj._arr[self._slot] + raw = (view & self._mask) >> self._shift + if obj._arr.ndim == 0: + return self._enum(int(raw)) + return raw -class _FieldBatch: - """Batch counterpart of ``_Field``. Returns the 1-D ``NDArray`` for that field.""" +_BITFIELD_TYPES = (_BitFlag, _GroupMask) +_DECLARATION_TYPES = (_Field, _BitFlag, _GroupMask) - def __init__(self, field_name: str) -> None: - self._field = field_name +# value/raw_payload deliberately omitted: overriding them is the intended +# pattern for single-slot converter-driven payloads. +_RESERVED_FIELD_NAMES = frozenset({"_arr", "_dtype", "_repr_fields"}) - def __set_name__(self, owner: object, name: str) -> None: - self._name = name - def __get__(self, obj: "PayloadBase | None", owner: object = None) -> np.ndarray: - if obj is None: - return self # type: ignore[return-value] - return obj._arr[self._field] - - -# Pairing table used by ``__init_subclass__`` to derive ``.Batch``. -def _swap_to_batch(val: object) -> object | None: - if isinstance(val, _BitFlag): - return _BitFlagBatch(val._mask) - if isinstance(val, _GroupMask): - return _GroupMaskBatch(val._mask, val._shift, val._enum) - if isinstance(val, _Field): - return _FieldBatch(val._field) - return None - - -_SCALAR_BITFIELD_TYPES = (_BitFlag, _GroupMask) -_BATCH_BITFIELD_TYPES = (_BitFlagBatch, _GroupMaskBatch) -_GROUP_TYPES = (_GroupMask, _GroupMaskBatch) -_FLAG_TYPES = (_BitFlag, _BitFlagBatch) +class PayloadBase(Generic[NpStructT]): + """Base class for typed Harp register payloads.""" + _dtype: ClassVar[np.dtype] + _repr_fields: ClassVar[tuple[str, ...]] + _arr: NDArray[NpStructT] -# --------------------------------------------------------------------------- -# PayloadBase -# --------------------------------------------------------------------------- + def __init__(self, *args: object, **kwargs: object) -> None: + cls = type(self) + names = self._dtype.names + assert names is not None + if args and kwargs: + raise TypeError( + f"{cls.__name__}() does not accept positional and keyword args together" + ) + if args: + if len(args) != 1 or len(names) != 1: + raise TypeError(f"{cls.__name__}() takes exactly one positional argument") + kwargs = {names[0]: args[0]} -class PayloadBase(Generic[NpStructT]): - """Base class for typed Harp register payloads. + slot_kwargs = {k: v for k, v in kwargs.items() if k in names} + descriptor_kwargs = {k: v for k, v in kwargs.items() if k not in names} - Storage contract for ``_arr``: + bitfields = cls._collect_bitfields() + unknown = set(descriptor_kwargs) - set(bitfields) + if unknown: + raise TypeError(f"{cls.__name__}() got unexpected kwargs: {sorted(unknown)}") - * ``ndim == 0`` — single record (produced by ``__init__`` and by - ``RegisterBase.parse``). Descriptor reads return Python / 0-D scalars. - * ``ndim == 1`` — batch of N records (produced by - ``RegisterBase.read_frames``). Lives on the ``.Batch`` sibling class - whose descriptors return ``ndarray``. + arr = np.zeros((), dtype=self._dtype) - ``_dtype`` is **always** a structured dtype. When a subclass declares a - primitive or sub-array dtype, ``__init_subclass__`` auto-promotes it to a - single-field structured dtype with the field name ``"value"``. + for name in names: + if name not in slot_kwargs: + continue + desc = cls._mro_descriptor(name) + if isinstance(desc, _Field): + desc._converter.encode_into(arr[name], slot_kwargs[name]) + else: + arr[name] = slot_kwargs[name] + + for attr_name, value in descriptor_kwargs.items(): + desc = bitfields[attr_name] + slot = desc._slot + mask_in_dtype = np.array(desc._mask, dtype=desc._dtype) + if isinstance(desc, _BitFlag): + if value: + arr[slot] |= mask_in_dtype + else: + shifted = np.array((int(value) << desc._shift) & desc._mask, dtype=desc._dtype) + arr[slot] = (arr[slot] & ~mask_in_dtype) | shifted - For multi-field structured payloads (e.g. ``AnalogData``), per-field - accessor descriptors are auto-generated from ``_dtype.names`` — codegen - does not need to declare them. - """ + self._arr = arr - _dtype: ClassVar[np.dtype] - _repr_fields: ClassVar[tuple[str, ...]] - Batch: ClassVar[type["PayloadBase"]] - _arr: NDArray[NpStructT] + @classmethod + def _mro_descriptor(cls, name: str) -> object | None: + for klass in cls.__mro__: + if name in klass.__dict__: + return klass.__dict__[name] + return None - def __init__(self, *args: object, **kwargs: object) -> None: - """Construct a single-record payload (``_arr`` is 0-D).""" - names = self._dtype.names - assert names is not None # invariant: __init_subclass__ promotes everything - if names == ("value",): - if len(args) != 1 or kwargs: - raise TypeError(f"{type(self).__name__}() takes exactly one positional argument") - self._arr = np.array((args[0],), dtype=self._dtype) - else: - if args: - raise TypeError( - f"{type(self).__name__}() requires keyword arguments, got positional args" - ) - unknown = set(kwargs) - set(names) - if unknown: - raise TypeError(f"{type(self).__name__}() got unexpected kwargs: {sorted(unknown)}") - values = tuple(kwargs[n] for n in names) - self._arr = np.array(values, dtype=self._dtype) + @classmethod + def _collect_bitfields(cls) -> dict[str, "_BitFlag | _GroupMask"]: + out: dict[str, _BitFlag | _GroupMask] = {} + for klass in reversed(cls.__mro__): + for attr, val in klass.__dict__.items(): + if isinstance(val, _BITFIELD_TYPES): + out[attr] = val + return out def __init_subclass__(cls, **kwargs: object) -> None: super().__init_subclass__(**kwargs) - # 1. Promote _dtype if it was provided as a primitive. - if "_dtype" in cls.__dict__: - raw = cls.__dict__["_dtype"] - if not isinstance(raw, np.dtype): - raw = np.dtype(raw) - if raw.names is None: - raw = np.dtype([("value", raw)]) - cls._dtype = raw - - # 2. Skip auto-derived Batch siblings so we don't recurse / re-pair. - if cls.__dict__.get("_is_batch_view", False): - return - - # 3. Multi-field struct payloads: auto-generate _Field accessors for - # any name that the user hasn't already declared. - has_bitfield = any(isinstance(val, _SCALAR_BITFIELD_TYPES) for val in vars(cls).values()) - names = cls._dtype.names - if not has_bitfield and names is not None and names != ("value",): - for name in names: - if name not in vars(cls): - setattr(cls, name, _Field(name)) - - # 4. Auto-derive _repr_fields if not explicitly set on this class. + for name, val in cls.__dict__.items(): + if isinstance(val, _DECLARATION_TYPES) and name in _RESERVED_FIELD_NAMES: + raise TypeError(f"{cls.__name__}: field name {name!r} is reserved by PayloadBase") + + own_declarations = [ + (name, val) for name, val in cls.__dict__.items() if isinstance(val, _DECLARATION_TYPES) + ] + + if own_declarations: + slots: dict[str, np.dtype] = {} + for attr_name, val in own_declarations: + if isinstance(val, _Field): + if val._name is None: + val._name = attr_name + slot, dtype = val._name, val._converter.dtype + else: + slot, dtype = val._slot, val._dtype + if slot in slots: + if slots[slot] != dtype: + raise TypeError( + f"{cls.__name__}: slot {slot!r} declared with conflicting " + f"dtypes {slots[slot]} and {dtype}" + ) + else: + slots[slot] = dtype + cls._dtype = np.dtype(list(slots.items())) + if "_repr_fields" not in cls.__dict__: bitfield_names = tuple( - name for name, val in vars(cls).items() if isinstance(val, _SCALAR_BITFIELD_TYPES) + name for name, val in vars(cls).items() if isinstance(val, _BITFIELD_TYPES) ) if bitfield_names: cls._repr_fields = bitfield_names - elif names is not None and names != ("value",): - cls._repr_fields = names else: - cls._repr_fields = ("value",) - - # 5. Synthesise the .Batch sibling by swapping each declared - # descriptor for its batch counterpart. If nothing to swap, the - # class is its own Batch (e.g. plain scalar PayloadU8). - batch_attrs: dict[str, object] = {"_is_batch_view": True} - for name, val in vars(cls).items(): - swapped = _swap_to_batch(val) - if swapped is not None: - batch_attrs[name] = swapped - - if len(batch_attrs) > 1: - cls.Batch = type(f"{cls.__name__}Batch", (cls,), batch_attrs) - else: - cls.Batch = cls - - # ------------------------------------------------------------------ - # Constructors - # ------------------------------------------------------------------ + names = cls._dtype.names if hasattr(cls, "_dtype") else None + if names is not None and names != ("value",): + cls._repr_fields = names + else: + cls._repr_fields = ("value",) @classmethod def from_array(cls, arr: "np.ndarray") -> Self: - """Wrap a pre-built numpy array of ``_dtype`` records. - - The shape of ``arr`` determines the storage mode: 0-D = single record - (used by ``parse``), 1-D = batch (used by ``read_frames``). For batch - construction in tests, call this on the ``.Batch`` sibling class. - """ obj = cls.__new__(cls) obj._arr = arr return obj @classmethod def from_buffer(cls, buf: bytes | bytearray | memoryview) -> Self: - """Decode a packed byte buffer into a batch payload (``_arr.ndim == 1``). - - Always returns an instance of ``cls.Batch`` so descriptor reads - produce ``ndarray`` columns regardless of the calling class. Use - :py:meth:`RegisterBase.parse` for the single-record / scalar path. - """ arr = np.frombuffer(buf, dtype=cls._dtype) - batch_cls = cls.Batch - obj = batch_cls.__new__(batch_cls) + obj = cls.__new__(cls) obj._arr = arr - return obj # type: ignore[return-value] - - # ------------------------------------------------------------------ - # Value accessors - # ------------------------------------------------------------------ + return obj @property def value(self) -> "NDArray[NpStructT]": - """Return the underlying value(s). - - For single-field ``("value",)`` payloads: - * 0-D _arr → 0-D numpy scalar (``np.uint16(1216)``) for primitive - registers, or 1-D sub-array for fixed-length array registers. - * 1-D _arr → 1-D ndarray (or 2-D for array registers). - - For multi-field structured payloads, returns the structured ``_arr`` - unchanged. - """ arr = self._arr if arr.dtype.names == ("value",): return arr["value"] @@ -300,51 +283,45 @@ def value(self) -> "NDArray[NpStructT]": @property def raw_payload(self) -> NDArray[NpStructT]: - """The backing ``_arr``. ``.tobytes()`` produces the wire payload.""" return self._arr - # ------------------------------------------------------------------ - # DataFrame - # ------------------------------------------------------------------ - def to_dataframe(self, *, decode_enums: bool = True) -> pd.DataFrame: - """Convert to a DataFrame. Works on both 0-D (1 row) and 1-D (N rows) ``_arr``.""" arr = np.atleast_1d(self._arr) cls = type(self) repr_fields = self._repr_fields - # Bitfield path — at least one repr field is a bit/group descriptor. - has_bitfield = any( - isinstance( - inspect.getattr_static(cls, f, None), _SCALAR_BITFIELD_TYPES + _BATCH_BITFIELD_TYPES - ) - for f in repr_fields - ) + has_bitfield = any(isinstance(cls._mro_descriptor(f), _BITFIELD_TYPES) for f in repr_fields) if has_bitfield: cols: dict[str, object] = {} - value_col = arr["value"] for f in repr_fields: - desc = inspect.getattr_static(cls, f, None) - if isinstance(desc, _GROUP_TYPES): - raw = (value_col & desc._mask) >> desc._shift + desc = cls._mro_descriptor(f) + if isinstance(desc, _GroupMask): + slot_col = arr[desc._slot] + raw = (slot_col & desc._mask) >> desc._shift if decode_enums: codes = desc._code_lookup[raw] cols[f] = pd.Categorical.from_codes(codes, categories=desc._categories) else: cols[f] = raw - elif isinstance(desc, _FLAG_TYPES): - cols[f] = (value_col & desc._mask) != 0 + elif isinstance(desc, _BitFlag): + slot_col = arr[desc._slot] + cols[f] = (slot_col & desc._mask) != 0 else: - # Custom property / hand-written field. Lift via atleast_1d - # so 0-D scalars become 1-row columns. cols[f] = np.atleast_1d(getattr(self, f)) return pd.DataFrame(cols) - # Structured path — walk dtype fields, expand sub-arrays. cols = {} names = self._dtype.names - single_value_field = names == ("value",) + single_value_slot = names == ("value",) for name in names: + desc = cls._mro_descriptor(name) + uses_converter = isinstance(desc, _Field) and not isinstance( + desc._converter, _IdentityConverter + ) + if uses_converter: + cols[name] = np.atleast_1d(getattr(self, name)) + continue + field_dtype, _ = self._dtype.fields[name] sub = arr[name] if field_dtype.subdtype is None: @@ -354,14 +331,10 @@ def to_dataframe(self, *, decode_enums: bool = True) -> pd.DataFrame: count = int(np.prod(subshape)) flat = sub.reshape(len(arr), count) for i in range(count): - col = str(i) if single_value_field else f"{name}_{i}" + col = str(i) if single_value_slot else f"{name}_{i}" cols[col] = flat[:, i] return pd.DataFrame(cols) - # ------------------------------------------------------------------ - # Dunder - # ------------------------------------------------------------------ - def __len__(self) -> int: return 1 if self._arr.ndim == 0 else len(self._arr) @@ -375,96 +348,82 @@ def __str__(self) -> str: return repr(self) -# ------------------------------------------------------------------ -# Named scalar payload classes — one per PayloadType. -# These are the concrete types returned by RegisterU8, RegisterU16, etc. -# ------------------------------------------------------------------ - - class PayloadU8(PayloadBase[np.uint8]): - _dtype: ClassVar = np.dtype("u1") + value = _Field(_IdentityConverter(np.dtype("u1"))) class PayloadU16(PayloadBase[np.uint16]): - _dtype: ClassVar = np.dtype(" P: - """Parse a single message into a scalar-typed payload (``_arr.ndim == 0``). - - Reads exactly one record's worth of bytes via ``np.frombuffer(..., - count=1)[0]``; descriptors on the resulting payload return Python / - 0-D scalars (``int``, ``bool``, ``IntEnum``, etc.). Use - :py:meth:`read_frames` for bulk file/buffer reads. - """ + """Parse a single message into a 0-D payload.""" buf = value.payload if isinstance(value, HarpMessage) else value record = np.frombuffer(buf, dtype=cls.payload_class._dtype, count=1)[0] return cast(P, cls.payload_class.from_array(record)) @@ -93,22 +87,16 @@ def _parse_buffer( *, parse_timestamp: bool = True, ) -> "tuple[np.ndarray, np.ndarray | None, np.ndarray | None, P]": - """Internal: parse a buffer once, returning (data, timestamps, msgtype_view, payload). - - Always wraps the result in ``payload_class.Batch`` (ndarray-typed - accessors). ``timestamps`` is None when the register is not - timestamped or when ``parse_timestamp`` is False. ``msgtype_view`` - is a strided uint8 view (zero-copy, always computed). Returns - ``data`` so its lifetime anchors the strided views. - """ - batch_cls = cls.payload_class.Batch + # Returns (data, timestamps, msgtype_view, payload). ``data`` is + # returned so its lifetime anchors the zero-copy strided views. + payload_cls = cls.payload_class if isinstance(source, (str, Path)): data = np.fromfile(source, dtype=np.uint8) else: data = np.frombuffer(source, dtype=np.uint8) if len(data) == 0: - payload = batch_cls.from_array(np.empty(0, dtype=batch_cls._dtype)) + payload = payload_cls.from_array(np.empty(0, dtype=payload_cls._dtype)) return data, None, None, cast(P, payload) stride = int(data[1]) + 2 @@ -125,19 +113,15 @@ def _parse_buffer( msgtype_view = np.ndarray(nrows, dtype=np.uint8, buffer=data, offset=0, strides=stride) - # Single zero-copy strided view: one structured record per frame. - # Sub-array fields (array registers) and multi-field payloads are - # both byte-packed in the file, so the structured dtype itemsize - # equals the payload byte length. payload_arr = np.ndarray( nrows, - dtype=batch_cls._dtype, + dtype=payload_cls._dtype, buffer=data, offset=payload_offset, strides=stride, ) - payload = batch_cls.from_array(payload_arr) + payload = payload_cls.from_array(payload_arr) return data, timestamps, msgtype_view, cast(P, payload) @classmethod @@ -145,25 +129,10 @@ def read_frames( cls, source: bytes | bytearray | memoryview | Path | str, ) -> "tuple[np.ndarray, P]": - """Read all frames from a single-register Harp binary buffer or file. - - Parameters - ---------- - source: - Raw bytes, a bytes-like object, or a path to a ``.bin`` file - containing packed Harp frames for a single register. - - Returns - ------- - timestamps : np.ndarray - 1-D float64 array of timestamps in seconds, one per frame. - For non-timestamped registers, a synthetic ``arange(N)`` is - returned. - payload : P - Instance of ``payload_class.Batch`` whose ``_arr`` is a - zero-copy strided view into the raw buffer (shape ``(N,)`` for - scalar/bitfield registers, ``(N, length)`` for array - registers). Descriptor reads return ``ndarray`` columns. + """Read all frames from a single-register binary buffer or file. + + Returns ``(timestamps, payload)``. For non-timestamped registers a + synthetic ``arange(N)`` is returned. """ _data, timestamps, _msg, payload = cls._parse_buffer(source, parse_timestamp=True) if timestamps is None: @@ -179,22 +148,12 @@ def read_dataframe( message_type: bool = False, decode_enums: bool = True, ) -> "pd.DataFrame": - """One-call read: parse all frames into a DataFrame. - - Parameters - ---------- - timestamp: - Insert a ``timestamp`` column (float seconds). For non-timestamped - registers this falls back to a synthetic frame index. - message_type: - Insert a ``message_type`` column as a ``pd.Categorical`` with - categories ``["Read", "Write", "Event"]`` (high bits masked off). - decode_enums: - If True (default), ``_GroupMask`` payload fields are decoded to - ``pd.Categorical`` columns using the enum member names. - Set False for raw integer columns and minimum overhead. - """ + """Parse all frames into a DataFrame. + ``timestamp`` and ``message_type`` insert leading columns. + ``decode_enums`` controls whether ``_GroupMask`` slots become + ``pd.Categorical`` (True) or raw integers (False). + """ _data, timestamps, msg_view, payload = cls._parse_buffer(source, parse_timestamp=timestamp) df = payload.to_dataframe(decode_enums=decode_enums) if message_type and msg_view is not None: @@ -250,13 +209,10 @@ def format( else: mt = MessageType.Write if message_type is None else message_type if isinstance(value, PayloadBase): - # Payload instance — use its backing array bytes directly raw = value.raw_payload.tobytes() elif isinstance(value, np.ndarray) and value.dtype != cls.payload_type.numpy_dtype: - # Structured numpy array passed by hand raw = value.tobytes() else: - # Scalar or array castable to the register's primitive dtype raw = np.asarray(value, dtype=cls.payload_type.numpy_dtype).tobytes() return build_message_frame( mt, cls.address, cls.payload_type, raw, port=port, timestamp=timestamp @@ -312,15 +268,15 @@ class _ArrayRegisterMeta(ABCMeta): """Calling with address and length creates a concrete subclass: ``RegisterU16Array(0x28, length=3)``.""" def __call__(cls: "type[_AR]", address: int, *, length: int) -> "type[_AR]": # type: ignore[override, misc] + from ._payload import _Field, _IdentityConverter + base_payload = cls.payload_class # type: ignore[attr-defined] - # ``base_payload._dtype`` is the auto-promoted single-field structured - # dtype ``[("value", primitive)]``; extract the primitive and rebuild - # with a sub-array field of the requested length. inner = base_payload._dtype.fields["value"][0] + sub_dtype = np.dtype((inner, (length,))) concrete_payload = type( f"{base_payload.__name__}_{length}", (base_payload,), - {"_dtype": np.dtype([("value", inner, (length,))])}, + {"value": _Field(_IdentityConverter(sub_dtype), name="value")}, ) return cast( "type[_AR]", diff --git a/tests/protocol/test_converter.py b/tests/protocol/test_converter.py new file mode 100644 index 0000000..515d0c8 --- /dev/null +++ b/tests/protocol/test_converter.py @@ -0,0 +1,215 @@ +"""Tests for the unified _Field + _Converter machinery in _payload.py. + +Covers: +* IdentityConverter via auto-generated _Field (parity with previous _Field). +* StringConverter (sub-array uint8 ↔ str). +* EnumConverter (full-byte enum decoding). +* Declarations-build-dtype direction (no _dtype on the subclass). +* Reserved-name collision check. +""" + +import enum + +import numpy as np +import pytest +from harp.protocol._payload import ( + PayloadBase, + _BitFlag, + _Field, + _GroupMask, + _IdentityConverter, + _StringConverter, +) + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +class _Color(enum.IntEnum): + Red = 0 + Green = 1 + Blue = 2 + + +# --------------------------------------------------------------------------- +# IdentityConverter — pass-through field +# --------------------------------------------------------------------------- + + +class _NumericPayload(PayloadBase): + a = _Field(_IdentityConverter(" NDArray[np.int16]: - return self.raw_payload["x"] - - @property - def y(self) -> NDArray[np.uint8]: - return self.raw_payload["y"] + x = _Field(_IdentityConverter(" pd.DataFrame: return pd.DataFrame( diff --git a/tests/protocol/test_register.py b/tests/protocol/test_register.py index 694c75d..f79b590 100644 --- a/tests/protocol/test_register.py +++ b/tests/protocol/test_register.py @@ -17,6 +17,8 @@ PayloadU16, PayloadU32, PayloadU64, + _Field, + _IdentityConverter, ) from harp.protocol._payload_type import PayloadType from harp.protocol._register import ( @@ -33,8 +35,6 @@ RegisterU32Array, RegisterU64, ) -from numpy.typing import NDArray - # --------------------------------------------------------------------------- # Fixtures / helpers # --------------------------------------------------------------------------- @@ -49,25 +49,9 @@ class DigitalOutputSet(RegisterU16): class AnalogDataPayload(PayloadBase): - _dtype: ClassVar = np.dtype( - [ - ("analog_input0", " NDArray[np.int16]: - return self._arr["analog_input0"] - - @property - def encoder(self) -> NDArray[np.int16]: - return self._arr["encoder"] - - @property - def analog_input1(self) -> NDArray[np.int16]: - return self._arr["analog_input1"] + analog_input0 = _Field(_IdentityConverter(" Date: Sun, 10 May 2026 22:11:08 -0700 Subject: [PATCH 207/267] Refactor payload converters and register handling to unify scalar and batch processing --- src/harp-device/src/harp/device/_registers.py | 33 +- src/harp-protocol/harp/protocol/_payload.py | 296 ++++++++++++++---- src/harp-protocol/harp/protocol/_register.py | 9 +- tests/protocol/test_converter.py | 8 +- tests/protocol/test_register.py | 19 +- 5 files changed, 267 insertions(+), 98 deletions(-) diff --git a/src/harp-device/src/harp/device/_registers.py b/src/harp-device/src/harp/device/_registers.py index 8ce4a99..99f52cf 100644 --- a/src/harp-device/src/harp/device/_registers.py +++ b/src/harp-device/src/harp/device/_registers.py @@ -2,13 +2,12 @@ from typing import ClassVar import numpy as np -import pandas as pd from harp.protocol._payload import ( PayloadBase, _BitFlag, _Field, _GroupMask, - _IdentityConverter, + _StringConverter, ) from harp.protocol._payload_type import PayloadType from harp.protocol._register import RegisterBase, RegisterU8, RegisterU16, RegisterU32 @@ -58,37 +57,13 @@ class ResetDevicePayload(PayloadBase[np.uint8]): class DeviceNamePayload(PayloadBase[np.uint8]): """Payload for the DeviceName register (address 12). - Stores a user-specified ASCII device name padded to 25 bytes. - Access the decoded string via ``.name``; ``.value`` (inherited) returns - the raw byte sub-array. + Stores a user-specified ASCII device name padded to 25 bytes. Encoding, + decoding, and the dataframe column are all handled by ``_StringConverter``. """ _MAX_LEN: ClassVar[int] = 25 - value = _Field(_IdentityConverter(np.dtype((np.uint8, (_MAX_LEN,))))) - - _repr_fields: ClassVar = ("name",) - - def __init__(self, name: str) -> None: - encoded = name.encode("ascii")[: self._MAX_LEN] - padded = encoded.ljust(self._MAX_LEN, b"\x00") - arr = np.zeros((), dtype=self._dtype) - arr["value"] = np.frombuffer(padded, dtype="u1") - self._arr = arr - - @property - def name(self) -> str: - # 0-D _arr (parse / __init__): _arr["value"] is shape (_MAX_LEN,). - # 1-D _arr (batch): take row 0 — Batch users should iterate rows - # explicitly via _arr["value"] if they need every name. - raw = self._arr["value"] if self._arr.ndim == 0 else self._arr["value"][0] - return raw.tobytes().rstrip(b"\x00").decode("ascii") - - def to_dataframe(self) -> pd.DataFrame: - if self._arr.ndim == 0: - return pd.DataFrame({"name": [self.name]}) - names = [row.tobytes().rstrip(b"\x00").decode("ascii") for row in self._arr["value"]] - return pd.DataFrame({"name": names}) + value = _Field(_StringConverter(_MAX_LEN)) class ClockConfigPayload(PayloadBase[np.uint8]): diff --git a/src/harp-protocol/harp/protocol/_payload.py b/src/harp-protocol/harp/protocol/_payload.py index 20a95a6..92ff3f1 100644 --- a/src/harp-protocol/harp/protocol/_payload.py +++ b/src/harp-protocol/harp/protocol/_payload.py @@ -1,5 +1,6 @@ +import enum from abc import ABC, abstractmethod -from typing import Any, ClassVar, Generic, TypeVar, final +from typing import Any, ClassVar, Generic, TypeVar, final, overload import numpy as np import pandas as pd @@ -7,14 +8,22 @@ from typing_extensions import Self NpStructT = TypeVar("NpStructT", bound=np.generic) +T = TypeVar("T") +E = TypeVar("E", bound=enum.IntEnum) +NpScalarT = TypeVar("NpScalarT", bound=np.generic) -class _Converter(ABC): +# --------------------------------------------------------------------------- +# Converters +# --------------------------------------------------------------------------- + + +class _Converter(ABC, Generic[T]): dtype: np.dtype python_type: type @abstractmethod - def decode_scalar(self, view: Any) -> Any: ... + def decode_scalar(self, view: Any) -> T: ... @abstractmethod def decode_batch(self, view: Any) -> Any: ... @@ -23,22 +32,22 @@ def decode_batch(self, view: Any) -> Any: ... def encode_into(self, view: Any, value: Any) -> None: ... -class _IdentityConverter(_Converter): - def __init__(self, dtype: "np.dtype | str | type") -> None: +class _IdentityConverter(_Converter[NpScalarT]): + def __init__(self, dtype: "np.dtype[NpScalarT] | str | type[NpScalarT]") -> None: self.dtype = np.dtype(dtype) self.python_type = self.dtype.type - def decode_scalar(self, view: Any) -> Any: + def decode_scalar(self, view: Any) -> NpScalarT: return view - def decode_batch(self, view: Any) -> Any: + def decode_batch(self, view: Any) -> "NDArray[NpScalarT]": return view - def encode_into(self, view: Any, value: Any) -> None: + def encode_into(self, view: Any, value: NpScalarT) -> None: view[...] = value -class _StringConverter(_Converter): +class _StringConverter(_Converter[str]): python_type = str def __init__(self, length: int, encoding: str = "ascii") -> None: @@ -61,8 +70,17 @@ def encode_into(self, view: Any, value: str) -> None: view[...] = np.frombuffer(padded, dtype=np.uint8) -class _Field: - def __init__(self, converter: _Converter, *, name: str | None = None) -> None: +# --------------------------------------------------------------------------- +# Descriptors — scalar variants (return Python / 0-D types) +# --------------------------------------------------------------------------- +# A PayloadBase subclass declares fields with the scalar descriptors below. +# `__init_subclass__` auto-derives a Batch sibling whose descriptors are +# swapped to the matching ``*Batch`` counterpart via ``_to_batch()`` and +# return ``NDArray`` views instead. + + +class _Field(Generic[T]): + def __init__(self, converter: _Converter[T], *, name: str | None = None) -> None: self._converter = converter self._name = name @@ -70,13 +88,17 @@ def __set_name__(self, owner: object, name: str) -> None: if self._name is None: self._name = name + @overload + def __get__(self, obj: None, owner: object = None) -> "_Field[T]": ... + @overload + def __get__(self, obj: "PayloadBase", owner: object = None) -> T: ... def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: if obj is None: return self - view = obj._arr[self._name] - if obj._arr.ndim == 0: - return self._converter.decode_scalar(view) - return self._converter.decode_batch(view) + return self._converter.decode_scalar(obj._arr[self._name]) + + def _to_batch(self) -> "_FieldBatch[T]": + return _FieldBatch(self._converter, name=self._name) class _BitFlag: @@ -91,20 +113,21 @@ def __init__( self._slot = slot self._dtype = np.dtype(dtype) - def __get__( - self, obj: "PayloadBase | None", owner: object = None - ) -> "bool | NDArray[np.bool_]": + @overload + def __get__(self, obj: None, owner: object = None) -> "_BitFlag": ... + @overload + def __get__(self, obj: "PayloadBase", owner: object = None) -> bool: ... + def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: if obj is None: - return self # type: ignore[return-value] - view = obj._arr[self._slot] - result = (view & self._mask) != 0 - if obj._arr.ndim == 0: - return bool(result) - return result + return self + return bool(obj._arr[self._slot] & self._mask) + def _to_batch(self) -> "_BitFlagBatch": + return _BitFlagBatch(self._mask, slot=self._slot, dtype=self._dtype) -def _build_enum_lookup(enum: type) -> "tuple[list[str], np.ndarray]": - members = list(enum) + +def _build_enum_lookup(enum_cls: type) -> "tuple[list[str], np.ndarray]": + members = list(enum_cls) categories = [m.name for m in members] max_val = max(int(m) for m in members) code_dtype = np.int8 if len(members) < 128 else np.int32 @@ -114,12 +137,95 @@ def _build_enum_lookup(enum: type) -> "tuple[list[str], np.ndarray]": return categories, code_lookup -class _GroupMask: +class _GroupMask(Generic[E]): + def __init__( + self, + mask: int, + shift: int, + enum: type[E], + *, + slot: str = "value", + dtype: "np.dtype | str | type" = np.uint8, + ) -> None: + self._mask = mask + self._shift = shift + self._enum = enum + self._slot = slot + self._dtype = np.dtype(dtype) + self._categories, self._code_lookup = _build_enum_lookup(enum) + + @overload + def __get__(self, obj: None, owner: object = None) -> "_GroupMask[E]": ... + @overload + def __get__(self, obj: "PayloadBase", owner: object = None) -> E: ... + def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: + if obj is None: + return self + raw = (obj._arr[self._slot] & self._mask) >> self._shift + return self._enum(int(raw)) + + def _to_batch(self) -> "_GroupMaskBatch[E]": + return _GroupMaskBatch( + self._mask, + self._shift, + self._enum, + slot=self._slot, + dtype=self._dtype, + ) + + +# --------------------------------------------------------------------------- +# Descriptors — batch variants (return ndarray views) +# --------------------------------------------------------------------------- + + +class _FieldBatch(Generic[T]): + def __init__(self, converter: _Converter[T], *, name: str | None = None) -> None: + self._converter = converter + self._name = name + + def __set_name__(self, owner: object, name: str) -> None: + if self._name is None: + self._name = name + + @overload + def __get__(self, obj: None, owner: object = None) -> "_FieldBatch[T]": ... + @overload + def __get__(self, obj: "PayloadBase", owner: object = None) -> "NDArray[Any]": ... + def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: + if obj is None: + return self + return self._converter.decode_batch(obj._arr[self._name]) + + +class _BitFlagBatch: + def __init__( + self, + mask: int, + *, + slot: str = "value", + dtype: "np.dtype | str | type" = np.uint8, + ) -> None: + self._mask = mask + self._slot = slot + self._dtype = np.dtype(dtype) + + @overload + def __get__(self, obj: None, owner: object = None) -> "_BitFlagBatch": ... + @overload + def __get__(self, obj: "PayloadBase", owner: object = None) -> "NDArray[np.bool_]": ... + def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: + if obj is None: + return self + return (obj._arr[self._slot] & self._mask) != 0 + + +class _GroupMaskBatch(Generic[E]): def __init__( self, mask: int, shift: int, - enum: type, + enum: type[E], *, slot: str = "value", dtype: "np.dtype | str | type" = np.uint8, @@ -131,29 +237,80 @@ def __init__( self._dtype = np.dtype(dtype) self._categories, self._code_lookup = _build_enum_lookup(enum) - def __get__(self, obj: "PayloadBase | None", owner: object = None): + @overload + def __get__(self, obj: None, owner: object = None) -> "_GroupMaskBatch[E]": ... + @overload + def __get__(self, obj: "PayloadBase", owner: object = None) -> "NDArray[np.signedinteger]": ... + def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: if obj is None: return self - view = obj._arr[self._slot] - raw = (view & self._mask) >> self._shift - if obj._arr.ndim == 0: - return self._enum(int(raw)) - return raw + return (obj._arr[self._slot] & self._mask) >> self._shift + + +_PT = TypeVar("_PT", bound="PayloadBase[Any]") + + +class Batch(Generic[_PT]): + """Phantom type for batched payloads. + + Statically, ``Batch[P]`` is a distinct type from ``P`` so the type + checker knows ``read_frames`` returns an ndarray-shaped view rather + than a single record. At runtime, the value is the auto-derived + ``P.Batch`` sibling whose descriptors return ``NDArray`` views. + + Per-field dtype precision is intentionally dropped — every declared + field reports ``NDArray[Any]`` — to keep ``RegisterBase[P]`` + parameterized by a single TypeVar. + """ + + raw_payload: "NDArray[Any]" + value: "NDArray[Any]" + + def __len__(self) -> int: ... # type: ignore[empty-body] + + def to_dataframe(self, *, decode_enums: bool = True) -> "pd.DataFrame": ... # type: ignore[empty-body] + def __getattr__(self, name: str) -> "NDArray[Any]": ... # type: ignore[empty-body] -_BITFIELD_TYPES = (_BitFlag, _GroupMask) -_DECLARATION_TYPES = (_Field, _BitFlag, _GroupMask) + +# Tuples used by isinstance() checks throughout the module. +_SCALAR_DECLARATION_TYPES = (_Field, _BitFlag, _GroupMask) +_BATCH_DECLARATION_TYPES = (_FieldBatch, _BitFlagBatch, _GroupMaskBatch) +_DECLARATION_TYPES = _SCALAR_DECLARATION_TYPES + _BATCH_DECLARATION_TYPES +_BITFIELD_TYPES = (_BitFlag, _GroupMask, _BitFlagBatch, _GroupMaskBatch) +_FIELD_TYPES = (_Field, _FieldBatch) +_GROUP_MASK_TYPES = (_GroupMask, _GroupMaskBatch) +_BIT_FLAG_TYPES = (_BitFlag, _BitFlagBatch) # value/raw_payload deliberately omitted: overriding them is the intended # pattern for single-slot converter-driven payloads. -_RESERVED_FIELD_NAMES = frozenset({"_arr", "_dtype", "_repr_fields"}) +_RESERVED_FIELD_NAMES = frozenset({"_arr", "_dtype", "_repr_fields", "Batch"}) + + +def _batch_init_disabled(self: "PayloadBase", *args: object, **kwargs: object) -> None: + raise TypeError( + f"{type(self).__name__} is a Batch payload; construct it via " + f"from_array()/from_buffer() (or use its scalar twin " + f"{type(self)._scalar_cls.__name__!s})." + ) class PayloadBase(Generic[NpStructT]): - """Base class for typed Harp register payloads.""" + """Base class for typed Harp register payloads. + + A subclass declares fields via the scalar descriptors above. + ``__init_subclass__`` auto-derives a ``Batch`` sibling subclass with the + same dtype but each descriptor swapped to a Batch variant returning an + ``NDArray`` view. ``from_array`` routes by ``ndim`` so callers never need + to mention the Batch class explicitly: 0-D records stay scalar, 1-D + buffers become Batch. + """ _dtype: ClassVar[np.dtype] _repr_fields: ClassVar[tuple[str, ...]] + _scalar_cls: ClassVar["type[PayloadBase]"] + _batch_cls: ClassVar["type[PayloadBase]"] + Batch: ClassVar["type[PayloadBase]"] _arr: NDArray[NpStructT] def __init__(self, *args: object, **kwargs: object) -> None: @@ -184,7 +341,7 @@ def __init__(self, *args: object, **kwargs: object) -> None: if name not in slot_kwargs: continue desc = cls._mro_descriptor(name) - if isinstance(desc, _Field): + if isinstance(desc, _FIELD_TYPES): desc._converter.encode_into(arr[name], slot_kwargs[name]) else: arr[name] = slot_kwargs[name] @@ -193,7 +350,7 @@ def __init__(self, *args: object, **kwargs: object) -> None: desc = bitfields[attr_name] slot = desc._slot mask_in_dtype = np.array(desc._mask, dtype=desc._dtype) - if isinstance(desc, _BitFlag): + if isinstance(desc, _BIT_FLAG_TYPES): if value: arr[slot] |= mask_in_dtype else: @@ -210,23 +367,42 @@ def _mro_descriptor(cls, name: str) -> object | None: return None @classmethod - def _collect_bitfields(cls) -> dict[str, "_BitFlag | _GroupMask"]: - out: dict[str, _BitFlag | _GroupMask] = {} + def _collect_bitfields( + cls, + ) -> "dict[str, _BitFlag | _GroupMask | _BitFlagBatch | _GroupMaskBatch]": + out: dict[str, Any] = {} for klass in reversed(cls.__mro__): for attr, val in klass.__dict__.items(): if isinstance(val, _BITFIELD_TYPES): out[attr] = val return out - def __init_subclass__(cls, **kwargs: object) -> None: + def __init_subclass__( + cls, + *, + _batch_of: "type[PayloadBase] | None" = None, + **kwargs: object, + ) -> None: super().__init_subclass__(**kwargs) + if _batch_of is not None: + # Auto-generated Batch sibling: borrow dtype/_repr_fields from its + # scalar twin and wire the scalar↔batch pointers. + cls._dtype = _batch_of._dtype + cls._repr_fields = _batch_of._repr_fields + cls._scalar_cls = _batch_of + cls._batch_cls = cls + _batch_of._batch_cls = cls + return + for name, val in cls.__dict__.items(): if isinstance(val, _DECLARATION_TYPES) and name in _RESERVED_FIELD_NAMES: raise TypeError(f"{cls.__name__}: field name {name!r} is reserved by PayloadBase") own_declarations = [ - (name, val) for name, val in cls.__dict__.items() if isinstance(val, _DECLARATION_TYPES) + (name, val) + for name, val in cls.__dict__.items() + if isinstance(val, _SCALAR_DECLARATION_TYPES) ] if own_declarations: @@ -250,7 +426,7 @@ def __init_subclass__(cls, **kwargs: object) -> None: if "_repr_fields" not in cls.__dict__: bitfield_names = tuple( - name for name, val in vars(cls).items() if isinstance(val, _BITFIELD_TYPES) + name for name, val in vars(cls).items() if isinstance(val, (_BitFlag, _GroupMask)) ) if bitfield_names: cls._repr_fields = bitfield_names @@ -261,18 +437,32 @@ def __init_subclass__(cls, **kwargs: object) -> None: else: cls._repr_fields = ("value",) + cls._scalar_cls = cls + cls._batch_cls = cls # rebound below once Batch is generated + + if hasattr(cls, "_dtype"): + batch_attrs: dict[str, Any] = {"__init__": _batch_init_disabled} + for name, val in cls.__dict__.items(): + if isinstance(val, _SCALAR_DECLARATION_TYPES): + batch_attrs[name] = val._to_batch() + cls.Batch = type( + f"{cls.__name__}Batch", + (cls,), + batch_attrs, + _batch_of=cls, + ) + @classmethod def from_array(cls, arr: "np.ndarray") -> Self: - obj = cls.__new__(cls) + target = cls._scalar_cls if arr.ndim == 0 else cls._batch_cls + obj = target.__new__(target) obj._arr = arr - return obj + return obj # type: ignore[return-value] @classmethod def from_buffer(cls, buf: bytes | bytearray | memoryview) -> Self: arr = np.frombuffer(buf, dtype=cls._dtype) - obj = cls.__new__(cls) - obj._arr = arr - return obj + return cls.from_array(arr) @property def value(self) -> "NDArray[NpStructT]": @@ -295,7 +485,7 @@ def to_dataframe(self, *, decode_enums: bool = True) -> pd.DataFrame: cols: dict[str, object] = {} for f in repr_fields: desc = cls._mro_descriptor(f) - if isinstance(desc, _GroupMask): + if isinstance(desc, _GROUP_MASK_TYPES): slot_col = arr[desc._slot] raw = (slot_col & desc._mask) >> desc._shift if decode_enums: @@ -303,7 +493,7 @@ def to_dataframe(self, *, decode_enums: bool = True) -> pd.DataFrame: cols[f] = pd.Categorical.from_codes(codes, categories=desc._categories) else: cols[f] = raw - elif isinstance(desc, _BitFlag): + elif isinstance(desc, _BIT_FLAG_TYPES): slot_col = arr[desc._slot] cols[f] = (slot_col & desc._mask) != 0 else: @@ -315,7 +505,7 @@ def to_dataframe(self, *, decode_enums: bool = True) -> pd.DataFrame: single_value_slot = names == ("value",) for name in names: desc = cls._mro_descriptor(name) - uses_converter = isinstance(desc, _Field) and not isinstance( + uses_converter = isinstance(desc, _FIELD_TYPES) and not isinstance( desc._converter, _IdentityConverter ) if uses_converter: diff --git a/src/harp-protocol/harp/protocol/_register.py b/src/harp-protocol/harp/protocol/_register.py index a8b75f8..62c0b06 100644 --- a/src/harp-protocol/harp/protocol/_register.py +++ b/src/harp-protocol/harp/protocol/_register.py @@ -22,6 +22,7 @@ class TimestampSecond(RegisterU32): from ._message import HarpMessage from ._message_type import MessageType from ._payload import ( + Batch, PayloadBase, PayloadFloat, PayloadFloatArray, @@ -86,7 +87,7 @@ def _parse_buffer( source: bytes | bytearray | memoryview | Path | str, *, parse_timestamp: bool = True, - ) -> "tuple[np.ndarray, np.ndarray | None, np.ndarray | None, P]": + ) -> "tuple[np.ndarray, np.ndarray | None, np.ndarray | None, Batch[P]]": # Returns (data, timestamps, msgtype_view, payload). ``data`` is # returned so its lifetime anchors the zero-copy strided views. payload_cls = cls.payload_class @@ -97,7 +98,7 @@ def _parse_buffer( if len(data) == 0: payload = payload_cls.from_array(np.empty(0, dtype=payload_cls._dtype)) - return data, None, None, cast(P, payload) + return data, None, None, cast("Batch[P]", payload) stride = int(data[1]) + 2 nrows = len(data) // stride @@ -122,13 +123,13 @@ def _parse_buffer( ) payload = payload_cls.from_array(payload_arr) - return data, timestamps, msgtype_view, cast(P, payload) + return data, timestamps, msgtype_view, cast("Batch[P]", payload) @classmethod def read_frames( cls, source: bytes | bytearray | memoryview | Path | str, - ) -> "tuple[np.ndarray, P]": + ) -> "tuple[np.ndarray, Batch[P]]": """Read all frames from a single-register binary buffer or file. Returns ``(timestamps, payload)``. For non-timestamped registers a diff --git a/tests/protocol/test_converter.py b/tests/protocol/test_converter.py index 515d0c8..90b1a43 100644 --- a/tests/protocol/test_converter.py +++ b/tests/protocol/test_converter.py @@ -183,7 +183,7 @@ class _Single(PayloadBase): def test_bitfield_payloads_ndim_aware(): - """One descriptor handles both 0-D and 1-D _arr — no Batch sibling.""" + """Scalar records stay on the declared class; batches route to the auto-derived ``Batch`` twin.""" class _Flags(PayloadBase): flag = _BitFlag(0x01, dtype=np.uint8) @@ -191,12 +191,14 @@ class _Flags(PayloadBase): # 0-D scalar record: flag=1, group bits=01 (Green) scalar = _Flags.from_array(np.array((0x03,), dtype=_Flags._dtype)) + assert type(scalar) is _Flags assert scalar.flag is True assert scalar.group is _Color.Green - # 1-D batch — same class, ndarray-typed accessors. + # 1-D batch — Batch sibling, ndarray-typed accessors. batch = _Flags.from_buffer(bytes([0x01, 0x02])) - assert type(batch) is _Flags + assert type(batch) is _Flags.Batch + assert isinstance(batch, _Flags) np.testing.assert_array_equal(batch.flag, [True, False]) np.testing.assert_array_equal(batch.group, [0, 1]) diff --git a/tests/protocol/test_register.py b/tests/protocol/test_register.py index f79b590..cbb6fff 100644 --- a/tests/protocol/test_register.py +++ b/tests/protocol/test_register.py @@ -410,14 +410,14 @@ def test_array_register_value_multi(): def test_parse_returns_zero_dim_arr(): - """parse() always wraps a single record in a 0-D _arr.""" + """parse() always wraps a single record in a 0-D _arr on the scalar class.""" frame = TimestampSecond.format(42) msg = _parse_frame(frame) parsed = TimestampSecond.parse(msg) assert parsed._arr.ndim == 0 assert isinstance(parsed, PayloadU32) - # All descriptors are ndim-aware — parse() and read_frames() both - # return the same payload class; there is no Batch sibling. + # parse() routes 0-D records to the scalar twin (PayloadU32); the auto- + # derived PayloadU32.Batch only handles 1-D buffers. assert type(parsed) is PayloadU32 @@ -431,17 +431,18 @@ def test_parse_does_not_overrun_buffer(): assert len(parsed) == 1 -def test_batch_payload_is_same_class_with_1d_arr(): - """from_buffer wraps a 1-D _arr in the same class — no Batch sibling. +def test_batch_payload_routes_to_batch_twin(): + """from_buffer wraps a 1-D _arr in the auto-derived ``Batch`` twin. - All descriptors inspect ``_arr.ndim`` at access time, so one class - handles both the 0-D scalar (``parse``) and 1-D batch (``read_frames`` - / ``from_buffer``) paths. + The Batch class is a subclass of the scalar class with each descriptor + swapped to its ``*Batch`` counterpart, so ``isinstance(batch, scalar_cls)`` + still holds while ``type(batch)`` is the Batch sibling. """ reg = RegisterU32Array(0x08, length=3) rows = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.dtype(" Date: Mon, 11 May 2026 13:37:40 -0700 Subject: [PATCH 208/267] Move framer away from protocol package --- src/harp-device/src/harp/device/__init__.py | 2 ++ .../harp/protocol => harp-device/src/harp/device}/_framer.py | 4 ++-- src/harp-device/src/harp/device/_serial.py | 3 ++- src/harp-protocol/harp/protocol/__init__.py | 3 --- src/harp-protocol/harp/protocol/_message.py | 2 +- tests/protocol/test_framer.py | 2 +- tests/protocol/test_payload.py | 1 - 7 files changed, 8 insertions(+), 9 deletions(-) rename src/{harp-protocol/harp/protocol => harp-device/src/harp/device}/_framer.py (96%) diff --git a/src/harp-device/src/harp/device/__init__.py b/src/harp-device/src/harp/device/__init__.py index f6fc2fd..6668b5b 100644 --- a/src/harp-device/src/harp/device/__init__.py +++ b/src/harp-device/src/harp/device/__init__.py @@ -1,4 +1,5 @@ from ._device import Device +from ._framer import HarpFramer from ._registers import ( AssemblyVersion, ClockConfig, @@ -23,6 +24,7 @@ __all__ = [ "Device", + "HarpFramer", "SerialDevice", "WhoAmI", "HwVersionH", diff --git a/src/harp-protocol/harp/protocol/_framer.py b/src/harp-device/src/harp/device/_framer.py similarity index 96% rename from src/harp-protocol/harp/protocol/_framer.py rename to src/harp-device/src/harp/device/_framer.py index c02a79d..d88e169 100644 --- a/src/harp-protocol/harp/protocol/_framer.py +++ b/src/harp-device/src/harp/device/_framer.py @@ -1,8 +1,8 @@ from collections.abc import Iterator from pathlib import Path -from ._message import HarpMessage, HarpParseError, parse -from ._message_type import from_byte as _validate_message_type +from harp.protocol._message import HarpMessage, HarpParseError, parse +from harp.protocol._message_type import from_byte as _validate_message_type class HarpFramer: diff --git a/src/harp-device/src/harp/device/_serial.py b/src/harp-device/src/harp/device/_serial.py index cc314e7..46d9f52 100644 --- a/src/harp-device/src/harp/device/_serial.py +++ b/src/harp-device/src/harp/device/_serial.py @@ -3,7 +3,8 @@ from typing import Any, ClassVar, Self, TypeVar import serial -from harp.protocol import HarpFramer, HarpMessage, MessageType +from harp.device._framer import HarpFramer +from harp.protocol import HarpMessage, MessageType from harp.protocol._message import ParsedHarpMessage from harp.protocol._payload import PayloadBase from harp.protocol._register import RegisterBase diff --git a/src/harp-protocol/harp/protocol/__init__.py b/src/harp-protocol/harp/protocol/__init__.py index 1fa83d5..312d666 100644 --- a/src/harp-protocol/harp/protocol/__init__.py +++ b/src/harp-protocol/harp/protocol/__init__.py @@ -1,6 +1,5 @@ from ._builder import build_message_frame from ._checksum import compute as compute_checksum, validate as validate_checksum -from ._framer import HarpFramer from ._message import HarpMessage, HarpParseError, ParsedHarpMessage, parse as parse_message from ._message_type import ( MessageType, @@ -69,8 +68,6 @@ "ParsedHarpMessage", "HarpParseError", "parse_message", - # Framer - "HarpFramer", # Payload DSL "PayloadBase", "PayloadU8", diff --git a/src/harp-protocol/harp/protocol/_message.py b/src/harp-protocol/harp/protocol/_message.py index f499383..6470716 100644 --- a/src/harp-protocol/harp/protocol/_message.py +++ b/src/harp-protocol/harp/protocol/_message.py @@ -40,7 +40,7 @@ def __init__( @classmethod def parse(cls, data: bytes | bytearray | memoryview) -> "HarpMessage": - """Parse and validate a complete Harp frame. Raises ``HarpParseError`` on failure.""" + """Parse and validate a complete Harp Message from a byte sequence. Raises ``HarpParseError`` on failure.""" raw = data if isinstance(data, bytes) else bytes(data) if len(raw) < 6: diff --git a/tests/protocol/test_framer.py b/tests/protocol/test_framer.py index a4ff279..5b8a6fc 100644 --- a/tests/protocol/test_framer.py +++ b/tests/protocol/test_framer.py @@ -1,6 +1,6 @@ import struct -from harp.protocol._framer import HarpFramer +from harp.device._framer import HarpFramer from harp.protocol._message_type import MessageType from tests.fixtures import TIMESTAMP_1S, make_frame_from_raw diff --git a/tests/protocol/test_payload.py b/tests/protocol/test_payload.py index 393b17a..a46a6a8 100644 --- a/tests/protocol/test_payload.py +++ b/tests/protocol/test_payload.py @@ -3,7 +3,6 @@ import pytest from harp.protocol._payload import PayloadBase, _Field, _IdentityConverter - class SimplePayload(PayloadBase): x = _Field(_IdentityConverter(" Date: Mon, 11 May 2026 13:45:38 -0700 Subject: [PATCH 209/267] Improve variable naming and documentation --- src/harp-device/src/harp/device/_framer.py | 2 +- src/harp-protocol/harp/protocol/__init__.py | 4 +- src/harp-protocol/harp/protocol/_builder.py | 2 +- src/harp-protocol/harp/protocol/_message.py | 64 +++++++++---------- .../harp/protocol/_message_type.py | 20 ++++-- tests/protocol/test_message_type.py | 20 +++--- 6 files changed, 61 insertions(+), 51 deletions(-) diff --git a/src/harp-device/src/harp/device/_framer.py b/src/harp-device/src/harp/device/_framer.py index d88e169..35b256e 100644 --- a/src/harp-device/src/harp/device/_framer.py +++ b/src/harp-device/src/harp/device/_framer.py @@ -2,7 +2,7 @@ from pathlib import Path from harp.protocol._message import HarpMessage, HarpParseError, parse -from harp.protocol._message_type import from_byte as _validate_message_type +from harp.protocol._message_type import message_type_from_byte as _validate_message_type class HarpFramer: diff --git a/src/harp-protocol/harp/protocol/__init__.py b/src/harp-protocol/harp/protocol/__init__.py index 312d666..adefb46 100644 --- a/src/harp-protocol/harp/protocol/__init__.py +++ b/src/harp-protocol/harp/protocol/__init__.py @@ -3,8 +3,8 @@ from ._message import HarpMessage, HarpParseError, ParsedHarpMessage, parse as parse_message from ._message_type import ( MessageType, - from_byte as message_type_from_byte, - to_byte as message_type_to_byte, + message_type_from_byte as message_type_from_byte, + message_type_to_byte as message_type_to_byte, ) from ._payload import ( PayloadBase, diff --git a/src/harp-protocol/harp/protocol/_builder.py b/src/harp-protocol/harp/protocol/_builder.py index ba932d7..e7b3d43 100644 --- a/src/harp-protocol/harp/protocol/_builder.py +++ b/src/harp-protocol/harp/protocol/_builder.py @@ -3,7 +3,7 @@ import struct from ._message_type import MessageType -from ._message_type import to_byte as _msg_type_byte +from ._message_type import message_type_to_byte as _msg_type_byte from ._payload_type import PayloadType, encode_payload_type diff --git a/src/harp-protocol/harp/protocol/_message.py b/src/harp-protocol/harp/protocol/_message.py index 6470716..be99921 100644 --- a/src/harp-protocol/harp/protocol/_message.py +++ b/src/harp-protocol/harp/protocol/_message.py @@ -1,11 +1,11 @@ """Harp message container.""" import struct -from typing import Any, Generic, TypeVar +from typing import Any, Generic, TypeVar, cast from ._builder import build_message_frame from ._checksum import validate as _validate_checksum -from ._message_type import MessageType +from ._message_type import MessageType, _message_type_from_byte_safe from ._payload import PayloadBase from ._payload_type import PayloadType, decode_payload_type @@ -22,19 +22,19 @@ class HarpMessage: Build with the constructor or parse from wire bytes with ``HarpMessage.parse()``. """ - __slots__ = ("_frame",) + __slots__ = ("_bytes",) def __init__( self, message_type: MessageType, address: int, payload_type: PayloadType, - payload: bytes = b"", + payload: "bytes" = b"", *, port: int = 0xFF, timestamp: float | None = None, ) -> None: - self._frame: bytes = build_message_frame( + self._bytes: "bytes" = build_message_frame( message_type, address, payload_type, payload, port=port, timestamp=timestamp ) @@ -51,10 +51,8 @@ def parse(cls, data: bytes | bytearray | memoryview) -> "HarpMessage": # Validate MessageType byte (bits 7,6,5,4,2 must be 0; bits 1:0 are type) b0 = raw[0] - if b0 & 0b11110100: - raise HarpParseError(f"Reserved bits set in MessageType byte: 0x{b0:02x}") - if (b0 & 0x03) not in (1, 2, 3): - raise HarpParseError(f"Invalid MessageType value in byte: 0x{b0:02x}") + if _message_type_from_byte_safe(b0) is None: + raise HarpParseError(f"Invalid MessageType byte: 0x{b0:02x}") length = raw[1] if len(raw) != length + 2: @@ -69,57 +67,64 @@ def parse(cls, data: bytes | bytearray | memoryview) -> "HarpMessage": raise HarpParseError("Frame too short to contain timestamp") obj = cls.__new__(cls) - obj._frame = raw + obj._bytes = raw return obj - # ------------------------------------------------------------------ - # Field accessors — direct bit reads into _frame, no copies - # ------------------------------------------------------------------ - @property def message_type(self) -> MessageType: - return MessageType(self._frame[0] & 0x03) + """Return the MessageType of this message.""" + return MessageType(self._bytes[0] & 0x03) @property def has_error(self) -> bool: - return bool(self._frame[0] & 0x08) + """Return True if the error flag is set in this message.""" + return bool(self._bytes[0] & 0x08) @property def address(self) -> int: - return self._frame[2] + """Return the address byte of this message.""" + return self._bytes[2] @property def port(self) -> int: - return self._frame[3] + """Return the port byte of this message.""" + return self._bytes[3] @property def payload_type(self) -> PayloadType: - return decode_payload_type(self._frame[4]).payload_type + """Return the PayloadType of this message.""" + return decode_payload_type(self._bytes[4]).payload_type @property def has_timestamp(self) -> bool: - return bool(self._frame[4] & 0x10) + """Return True if the timestamp flag is set in this message.""" + return bool(self._bytes[4] & 0x10) @property def timestamp(self) -> float | None: + """Return the timestamp of this message, or None if not present.""" if not self.has_timestamp: return None - seconds, microseconds = struct.unpack_from(" memoryview: """Payload bytes, excluding timestamp and checksum.""" offset = 11 if self.has_timestamp else 5 - return memoryview(self._frame)[offset:-1] + return memoryview(self._bytes)[offset:-1] + + @property + def bytes(self) -> bytes: + """The complete raw message frame, including checksum.""" + return self._bytes - def __repr__(self) -> str: + def __str__(self) -> str: return ( f"HarpMessage(message_type={self.message_type!r}, address={self.address:#04x}, " f"payload_type={self.payload_type!r}, timestamp={self.timestamp!r})" ) - class ParsedHarpMessage(HarpMessage, Generic[P]): """A ``HarpMessage`` with a typed parsed payload attached.""" @@ -145,11 +150,6 @@ def __init__( def from_message(cls, msg: HarpMessage, parsed: P) -> "ParsedHarpMessage[P]": """Wrap a ``HarpMessage`` with a pre-parsed payload.""" obj = cls.__new__(cls) - obj._frame = msg._frame + obj._bytes = msg.bytes obj.parsed = parsed - return obj - - -def parse(data: bytes | bytearray | memoryview) -> HarpMessage: - """Parse a complete Harp message frame. Alias for ``HarpMessage.parse()``.""" - return HarpMessage.parse(data) + return obj \ No newline at end of file diff --git a/src/harp-protocol/harp/protocol/_message_type.py b/src/harp-protocol/harp/protocol/_message_type.py index 324f984..247e741 100644 --- a/src/harp-protocol/harp/protocol/_message_type.py +++ b/src/harp-protocol/harp/protocol/_message_type.py @@ -12,16 +12,26 @@ class MessageType(IntEnum): _VALID_TYPES = frozenset(t.value for t in MessageType) -def from_byte(b: int) -> tuple["MessageType", bool]: - """Decode a MessageType byte into ``(MessageType, has_error)``. Raises ``ValueError`` on invalid input.""" +def _message_type_from_byte_safe(b: int) -> "tuple[MessageType, bool] | None": + """Decode a MessageType byte into ``(MessageType, has_error)``, or ``None`` if invalid.""" if b & _RESERVED_MASK: - raise ValueError(f"Reserved bits set in MessageType byte: 0x{b:02x}") + return None type_bits = b & 0x03 if type_bits not in _VALID_TYPES: - raise ValueError(f"Invalid MessageType value {type_bits} in byte: 0x{b:02x}") + return None return MessageType(type_bits), bool(b & 0x08) -def to_byte(message_type: MessageType, has_error: bool = False) -> int: +def message_type_from_byte(b: int) -> tuple["MessageType", bool]: + """Decode a MessageType byte into ``(MessageType, has_error)``. Raises ``ValueError`` on invalid input.""" + result = _message_type_from_byte_safe(b) + if result is None: + type_bits = b & 0x03 + if b & _RESERVED_MASK: + raise ValueError(f"Reserved bits set in MessageType byte: 0x{b:02x}") + raise ValueError(f"Invalid MessageType value {type_bits} in byte: 0x{b:02x}") + return result + +def message_type_to_byte(message_type: MessageType, has_error: bool = False) -> int: """Encode MessageType + error flag to a single byte.""" return message_type.value | (0x08 if has_error else 0) diff --git a/tests/protocol/test_message_type.py b/tests/protocol/test_message_type.py index 3bd4a08..ad2b781 100644 --- a/tests/protocol/test_message_type.py +++ b/tests/protocol/test_message_type.py @@ -1,5 +1,5 @@ import pytest -from harp.protocol._message_type import MessageType, from_byte, to_byte +from harp.protocol._message_type import MessageType, message_type_from_byte, message_type_to_byte @pytest.mark.parametrize( @@ -14,7 +14,7 @@ ], ) def test_from_byte_valid(byte, expected_type, expected_error): - msg_type, has_error = from_byte(byte) + msg_type, has_error = message_type_from_byte(byte) assert msg_type == expected_type assert has_error == expected_error @@ -33,24 +33,24 @@ def test_from_byte_valid(byte, expected_type, expected_error): ) def test_from_byte_invalid(byte): with pytest.raises(ValueError): - from_byte(byte) + message_type_from_byte(byte) def test_to_byte_no_error(): - assert to_byte(MessageType.Read) == 0x01 - assert to_byte(MessageType.Write) == 0x02 - assert to_byte(MessageType.Event) == 0x03 + assert message_type_to_byte(MessageType.Read) == 0x01 + assert message_type_to_byte(MessageType.Write) == 0x02 + assert message_type_to_byte(MessageType.Event) == 0x03 def test_to_byte_with_error(): - assert to_byte(MessageType.Read, has_error=True) == 0x09 - assert to_byte(MessageType.Write, has_error=True) == 0x0A + assert message_type_to_byte(MessageType.Read, has_error=True) == 0x09 + assert message_type_to_byte(MessageType.Write, has_error=True) == 0x0A def test_roundtrip(): for mt in MessageType: for err in (False, True): - b = to_byte(mt, err) - mt2, err2 = from_byte(b) + b = message_type_to_byte(mt, err) + mt2, err2 = message_type_from_byte(b) assert mt2 == mt assert err2 == err From cd920ef9655ac9c400f7c40919f5d5bd8f572052 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Mon, 11 May 2026 15:21:05 -0700 Subject: [PATCH 210/267] Remove redundant method and add doc string to register module --- src/harp-protocol/harp/protocol/_message.py | 3 +- .../harp/protocol/_message_type.py | 1 + .../harp/protocol/_payload_type.py | 2 +- src/harp-protocol/harp/protocol/_register.py | 138 ++++++++++-------- 4 files changed, 78 insertions(+), 66 deletions(-) diff --git a/src/harp-protocol/harp/protocol/_message.py b/src/harp-protocol/harp/protocol/_message.py index be99921..be432e6 100644 --- a/src/harp-protocol/harp/protocol/_message.py +++ b/src/harp-protocol/harp/protocol/_message.py @@ -125,6 +125,7 @@ def __str__(self) -> str: f"payload_type={self.payload_type!r}, timestamp={self.timestamp!r})" ) + class ParsedHarpMessage(HarpMessage, Generic[P]): """A ``HarpMessage`` with a typed parsed payload attached.""" @@ -152,4 +153,4 @@ def from_message(cls, msg: HarpMessage, parsed: P) -> "ParsedHarpMessage[P]": obj = cls.__new__(cls) obj._bytes = msg.bytes obj.parsed = parsed - return obj \ No newline at end of file + return obj diff --git a/src/harp-protocol/harp/protocol/_message_type.py b/src/harp-protocol/harp/protocol/_message_type.py index 247e741..623562e 100644 --- a/src/harp-protocol/harp/protocol/_message_type.py +++ b/src/harp-protocol/harp/protocol/_message_type.py @@ -32,6 +32,7 @@ def message_type_from_byte(b: int) -> tuple["MessageType", bool]: raise ValueError(f"Invalid MessageType value {type_bits} in byte: 0x{b:02x}") return result + def message_type_to_byte(message_type: MessageType, has_error: bool = False) -> int: """Encode MessageType + error flag to a single byte.""" return message_type.value | (0x08 if has_error else 0) diff --git a/src/harp-protocol/harp/protocol/_payload_type.py b/src/harp-protocol/harp/protocol/_payload_type.py index 855c6ef..c7f3264 100644 --- a/src/harp-protocol/harp/protocol/_payload_type.py +++ b/src/harp-protocol/harp/protocol/_payload_type.py @@ -19,7 +19,7 @@ class PayloadType(Enum): @property def numpy_dtype(self) -> np.dtype: - return self.value + return self.value # type: ignore @dataclass(frozen=True) diff --git a/src/harp-protocol/harp/protocol/_register.py b/src/harp-protocol/harp/protocol/_register.py index 62c0b06..5b3a52f 100644 --- a/src/harp-protocol/harp/protocol/_register.py +++ b/src/harp-protocol/harp/protocol/_register.py @@ -1,15 +1,3 @@ -"""Register base classes for the Harp protocol. - -Define a register by subclassing the appropriate typed class:: - - class TimestampSecond(RegisterU32): - address: ClassVar[int] = 8 - - TimestampSecond.format() # → Read request frame - TimestampSecond.format(42) # → Write request frame - TimestampSecond.parse(msg) # → PayloadU32 instance -""" - from abc import ABC, ABCMeta from pathlib import Path from typing import Any, ClassVar, Generic, TypeVar, cast, final, overload @@ -71,36 +59,36 @@ class RegisterBase(ABC, Generic[P]): address: ClassVar[int] payload_type: ClassVar[PayloadType] - payload_class: ClassVar[type[Any]] + payload_class: ClassVar[type[PayloadBase[Any]]] length: ClassVar[int | None] = None @classmethod def parse(cls, value: HarpMessage | bytes | bytearray | memoryview) -> P: - """Parse a single message into a 0-D payload.""" + """Parse a single message into a 0-D payload. It will only try to parse the first structured record if the payload is an array.""" buf = value.payload if isinstance(value, HarpMessage) else value - record = np.frombuffer(buf, dtype=cls.payload_class._dtype, count=1)[0] + record = np.frombuffer(buf, dtype=cls.payload_class.dtype, count=1)[0] return cast(P, cls.payload_class.from_array(record)) @classmethod - def _parse_buffer( + def parse_bulk( cls, - source: bytes | bytearray | memoryview | Path | str, + source: bytes | bytearray | memoryview, *, parse_timestamp: bool = True, ) -> "tuple[np.ndarray, np.ndarray | None, np.ndarray | None, Batch[P]]": + """Parse a bulk buffer containing one or more frames of this register type. Returns (data, timestamps, msgtype_view, payload).""" # Returns (data, timestamps, msgtype_view, payload). ``data`` is - # returned so its lifetime anchors the zero-copy strided views. payload_cls = cls.payload_class - if isinstance(source, (str, Path)): - data = np.fromfile(source, dtype=np.uint8) - else: - data = np.frombuffer(source, dtype=np.uint8) + data = np.frombuffer(source, dtype=np.uint8) if len(data) == 0: - payload = payload_cls.from_array(np.empty(0, dtype=payload_cls._dtype)) + # No frames, but still need to return a Batch with the right dtype. + payload = payload_cls.from_array(np.empty(0, dtype=payload_cls.dtype)) return data, None, None, cast("Batch[P]", payload) - stride = int(data[1]) + 2 + stride = ( + int(data[1]) + 2 + ) # TODO this assumes all frames have the same length but we may want to revisit in the future. nrows = len(data) // stride is_timestamped = bool(int(data[4]) & 0x10) payload_offset = 11 if is_timestamped else 5 @@ -109,6 +97,7 @@ def _parse_buffer( ts_s = np.ndarray(nrows, dtype=" "tuple[np.ndarray, Batch[P]]": - """Read all frames from a single-register binary buffer or file. - - Returns ``(timestamps, payload)``. For non-timestamped registers a - synthetic ``arange(N)`` is returned. - """ - _data, timestamps, _msg, payload = cls._parse_buffer(source, parse_timestamp=True) - if timestamps is None: - timestamps = np.arange(len(payload), dtype=np.float64) - return timestamps, payload - @classmethod def read_dataframe( cls, - source: bytes | bytearray | memoryview | Path | str, + source: bytes | bytearray | memoryview, *, timestamp: bool = True, message_type: bool = False, @@ -155,10 +129,10 @@ def read_dataframe( ``decode_enums`` controls whether ``_GroupMask`` slots become ``pd.Categorical`` (True) or raw integers (False). """ - _data, timestamps, msg_view, payload = cls._parse_buffer(source, parse_timestamp=timestamp) + _data, timestamps, msg_view, payload = cls.parse_bulk(source, parse_timestamp=timestamp) df = payload.to_dataframe(decode_enums=decode_enums) if message_type and msg_view is not None: - _msg_names = np.array(["", "Read", "Write", "Event"]) + _msg_names = np.array(["_NONE", "Read", "Write", "Event"]) df.insert( 0, "message_type", @@ -221,63 +195,81 @@ def format( class RegisterU8(RegisterBase[PayloadU8], metaclass=_RegisterMeta): + """A simple scalar register with a uint8 payload.""" + payload_type: ClassVar[PayloadType] = PayloadType.U8 - payload_class: ClassVar[type[PayloadU8]] = PayloadU8 + payload_class = PayloadU8 class RegisterU16(RegisterBase[PayloadU16], metaclass=_RegisterMeta): + """A simple scalar register with a uint16 payload.""" + payload_type: ClassVar[PayloadType] = PayloadType.U16 - payload_class: ClassVar[type[PayloadU16]] = PayloadU16 + payload_class = PayloadU16 class RegisterU32(RegisterBase[PayloadU32], metaclass=_RegisterMeta): + """A simple scalar register with a uint32 payload.""" + payload_type: ClassVar[PayloadType] = PayloadType.U32 - payload_class: ClassVar[type[PayloadU32]] = PayloadU32 + payload_class = PayloadU32 class RegisterU64(RegisterBase[PayloadU64], metaclass=_RegisterMeta): + """A simple scalar register with a uint64 payload.""" + payload_type: ClassVar[PayloadType] = PayloadType.U64 - payload_class: ClassVar[type[PayloadU64]] = PayloadU64 + payload_class = PayloadU64 class RegisterS8(RegisterBase[PayloadS8], metaclass=_RegisterMeta): + """A simple scalar register with a int8 payload.""" + payload_type: ClassVar[PayloadType] = PayloadType.S8 - payload_class: ClassVar[type[PayloadS8]] = PayloadS8 + payload_class = PayloadS8 class RegisterS16(RegisterBase[PayloadS16], metaclass=_RegisterMeta): + """A simple scalar register with a int16 payload.""" + payload_type: ClassVar[PayloadType] = PayloadType.S16 - payload_class: ClassVar[type[PayloadS16]] = PayloadS16 + payload_class = PayloadS16 class RegisterS32(RegisterBase[PayloadS32], metaclass=_RegisterMeta): + """A simple scalar register with a int32 payload.""" + payload_type: ClassVar[PayloadType] = PayloadType.S32 - payload_class: ClassVar[type[PayloadS32]] = PayloadS32 + payload_class = PayloadS32 class RegisterS64(RegisterBase[PayloadS64], metaclass=_RegisterMeta): + """A simple scalar register with a int64 payload.""" + payload_type: ClassVar[PayloadType] = PayloadType.S64 - payload_class: ClassVar[type[PayloadS64]] = PayloadS64 + payload_class = PayloadS64 class RegisterFloat(RegisterBase[PayloadFloat], metaclass=_RegisterMeta): + """A simple scalar register with a float32 payload.""" + payload_type: ClassVar[PayloadType] = PayloadType.Float - payload_class: ClassVar[type[PayloadFloat]] = PayloadFloat + payload_class = PayloadFloat class _ArrayRegisterMeta(ABCMeta): - """Calling with address and length creates a concrete subclass: ``RegisterU16Array(0x28, length=3)``.""" + """A base metaclass for array registers. Calling with address and length creates a concrete subclass: ``RegisterU16Array(0x28, length=3)``.""" def __call__(cls: "type[_AR]", address: int, *, length: int) -> "type[_AR]": # type: ignore[override, misc] from ._payload import _Field, _IdentityConverter base_payload = cls.payload_class # type: ignore[attr-defined] - inner = base_payload._dtype.fields["value"][0] + inner = base_payload.dtype.fields["value"][0] sub_dtype = np.dtype((inner, (length,))) concrete_payload = type( f"{base_payload.__name__}_{length}", (base_payload,), - {"value": _Field(_IdentityConverter(sub_dtype), name="value")}, + {"value": _Field(_IdentityConverter(sub_dtype), name="value")}, # we ) return cast( "type[_AR]", @@ -294,45 +286,63 @@ def __call__(cls: "type[_AR]", address: int, *, length: int) -> "type[_AR]": # class RegisterU8Array(RegisterBase[PayloadU8Array], metaclass=_ArrayRegisterMeta): + """A simple array register with a uint8 array payload. It must be instantiated with a length: ``RegisterU8Array(0x28, length=3)``.""" + payload_type: ClassVar[PayloadType] = PayloadType.U8 - payload_class: ClassVar[type[Any]] = PayloadU8Array + payload_class = PayloadU8Array class RegisterU16Array(RegisterBase[PayloadU16Array], metaclass=_ArrayRegisterMeta): + """A simple array register with a uint16 array payload. It must be instantiated with a length: ``RegisterU16Array(0x28, length=3)``.""" + payload_type: ClassVar[PayloadType] = PayloadType.U16 - payload_class: ClassVar[type[Any]] = PayloadU16Array + payload_class = PayloadU16Array class RegisterU32Array(RegisterBase[PayloadU32Array], metaclass=_ArrayRegisterMeta): + """A simple array register with a uint32 array payload. It must be instantiated with a length: ``RegisterU32Array(0x28, length=3)``.""" + payload_type: ClassVar[PayloadType] = PayloadType.U32 - payload_class: ClassVar[type[Any]] = PayloadU32Array + payload_class = PayloadU32Array class RegisterU64Array(RegisterBase[PayloadU64Array], metaclass=_ArrayRegisterMeta): + """A simple array register with a uint64 array payload. It must be instantiated with a length: ``RegisterU64Array(0x28, length=3)``.""" + payload_type: ClassVar[PayloadType] = PayloadType.U64 - payload_class: ClassVar[type[Any]] = PayloadU64Array + payload_class = PayloadU64Array class RegisterS8Array(RegisterBase[PayloadS8Array], metaclass=_ArrayRegisterMeta): + """A simple array register with a int8 array payload. It must be instantiated with a length: ``RegisterS8Array(0x28, length=3)``.""" + payload_type: ClassVar[PayloadType] = PayloadType.S8 - payload_class: ClassVar[type[Any]] = PayloadS8Array + payload_class = PayloadS8Array class RegisterS16Array(RegisterBase[PayloadS16Array], metaclass=_ArrayRegisterMeta): + """A simple array register with a int16 array payload. It must be instantiated with a length: ``RegisterS16Array(0x28, length=3)``.""" + payload_type: ClassVar[PayloadType] = PayloadType.S16 - payload_class: ClassVar[type[Any]] = PayloadS16Array + payload_class = PayloadS16Array class RegisterS32Array(RegisterBase[PayloadS32Array], metaclass=_ArrayRegisterMeta): + """A simple array register with a int32 array payload. It must be instantiated with a length: ``RegisterS32Array(0x28, length=3)``.""" + payload_type: ClassVar[PayloadType] = PayloadType.S32 - payload_class: ClassVar[type[Any]] = PayloadS32Array + payload_class = PayloadS32Array class RegisterS64Array(RegisterBase[PayloadS64Array], metaclass=_ArrayRegisterMeta): + """A simple array register with a int64 array payload. It must be instantiated with a length: ``RegisterS64Array(0x28, length=3)``.""" + payload_type: ClassVar[PayloadType] = PayloadType.S64 - payload_class: ClassVar[type[Any]] = PayloadS64Array + payload_class = PayloadS64Array class RegisterFloatArray(RegisterBase[PayloadFloatArray], metaclass=_ArrayRegisterMeta): + """A simple array register with a float32 array payload. It must be instantiated with a length: ``RegisterFloatArray(0x28, length=3)``.""" + payload_type: ClassVar[PayloadType] = PayloadType.Float - payload_class: ClassVar[type[Any]] = PayloadFloatArray + payload_class = PayloadFloatArray From 2882178d5803f96230b2a538b60cf1ffa49820b1 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Mon, 11 May 2026 17:06:45 -0700 Subject: [PATCH 211/267] Split scalar/array/struct payloads and type parse() return --- scripts/benchmark_analog_data.py | 16 +- scripts/core_registers_example.py | 2 +- src/harp-device/src/harp/device/_device.py | 3 +- src/harp-device/src/harp/device/_framer.py | 4 +- src/harp-device/src/harp/device/_serial.py | 3 +- src/harp-protocol/harp/protocol/__init__.py | 16 +- src/harp-protocol/harp/protocol/_message.py | 5 +- src/harp-protocol/harp/protocol/_payload.py | 283 ++++++++++-------- .../harp/protocol/_payload_converters.py | 116 +++++++ src/harp-protocol/harp/protocol/_register.py | 114 +++---- tests/protocol/test_converter.py | 16 +- tests/protocol/test_message.py | 21 +- tests/protocol/test_payload.py | 7 +- tests/protocol/test_register.py | 152 ++++------ tests/test_device.py | 12 +- 15 files changed, 457 insertions(+), 313 deletions(-) create mode 100644 src/harp-protocol/harp/protocol/_payload_converters.py diff --git a/scripts/benchmark_analog_data.py b/scripts/benchmark_analog_data.py index fda3026..7171c8a 100644 --- a/scripts/benchmark_analog_data.py +++ b/scripts/benchmark_analog_data.py @@ -27,20 +27,15 @@ from typing import ClassVar import numpy as np - -# --------------------------------------------------------------------------- -# Add repo root to sys.path so this script works whether invoked from root or -# from within scripts/. -# --------------------------------------------------------------------------- -REPO_ROOT = Path(__file__).parent.parent -sys.path.insert(0, str(REPO_ROOT)) - from scripts._harp_io import read as harp_read # vendored harp-python read() from harp.protocol._payload import PayloadBase, _Field, _IdentityConverter from harp.protocol._payload_type import PayloadType from harp.protocol._register import RegisterBase + +REPO_ROOT = Path(__file__).parent.parent + # --------------------------------------------------------------------------- # AnalogData register definition (hand-written; would come from codegen) # --------------------------------------------------------------------------- @@ -72,8 +67,9 @@ class AnalogData(RegisterBase[AnalogDataPayload]): def pyharp_read(path: Path, *, include_timestamp: bool = True): - """pyharp path: read_frames → to_dataframe.""" - timestamps, payload = AnalogData.read_frames(path) + """pyharp path: parse_bulk → to_dataframe.""" + bytes_2_parse = path.read_bytes() + _data, timestamps, msg_type, payload = AnalogData.parse_bulk(bytes_2_parse) df = payload.to_dataframe() if include_timestamp: df.insert(0, "timestamp", timestamps) diff --git a/scripts/core_registers_example.py b/scripts/core_registers_example.py index 5fd65a8..bd0074f 100644 --- a/scripts/core_registers_example.py +++ b/scripts/core_registers_example.py @@ -93,7 +93,7 @@ # --------------------------------------------------------------------------- BIN_FILE = Path(__file__).parent.parent / "notes/Behavior.harp/Behavior_10.bin" print(f"\n=== Bulk read from {BIN_FILE.name} ===") -timestamps, payload = OperationControl.read_frames(BIN_FILE) +_data, timestamps, msg_type, payload = OperationControl.parse_bulk(BIN_FILE.read_bytes()) print(f" {len(timestamps)} frame(s) read") print(f" timestamps (s) : {timestamps}") print(f" operation_mode : {payload.operation_mode}") diff --git a/src/harp-device/src/harp/device/_device.py b/src/harp-device/src/harp/device/_device.py index aa60cf8..dc49c54 100644 --- a/src/harp-device/src/harp/device/_device.py +++ b/src/harp-device/src/harp/device/_device.py @@ -3,12 +3,11 @@ from typing import Any, TypeVar from harp.protocol._message import ParsedHarpMessage -from harp.protocol._payload import PayloadBase from harp.protocol._register import RegisterBase from ._serial import SerialDevice -P = TypeVar("P", bound=PayloadBase[Any]) +P = TypeVar("P") class Device: diff --git a/src/harp-device/src/harp/device/_framer.py b/src/harp-device/src/harp/device/_framer.py index 35b256e..b7ec28f 100644 --- a/src/harp-device/src/harp/device/_framer.py +++ b/src/harp-device/src/harp/device/_framer.py @@ -1,7 +1,7 @@ from collections.abc import Iterator from pathlib import Path -from harp.protocol._message import HarpMessage, HarpParseError, parse +from harp.protocol._message import HarpMessage, HarpParseError from harp.protocol._message_type import message_type_from_byte as _validate_message_type @@ -66,7 +66,7 @@ def next_frame(self) -> HarpMessage | None: frame = bytes(buf[pos:frame_end]) try: - msg = parse(frame) + msg = HarpMessage.parse(frame) pos = frame_end self._pos = pos return msg diff --git a/src/harp-device/src/harp/device/_serial.py b/src/harp-device/src/harp/device/_serial.py index 46d9f52..7a8946a 100644 --- a/src/harp-device/src/harp/device/_serial.py +++ b/src/harp-device/src/harp/device/_serial.py @@ -6,10 +6,9 @@ from harp.device._framer import HarpFramer from harp.protocol import HarpMessage, MessageType from harp.protocol._message import ParsedHarpMessage -from harp.protocol._payload import PayloadBase from harp.protocol._register import RegisterBase -P = TypeVar("P", bound=PayloadBase[Any]) +P = TypeVar("P") class SerialDevice: diff --git a/src/harp-protocol/harp/protocol/__init__.py b/src/harp-protocol/harp/protocol/__init__.py index adefb46..ef21618 100644 --- a/src/harp-protocol/harp/protocol/__init__.py +++ b/src/harp-protocol/harp/protocol/__init__.py @@ -1,11 +1,18 @@ from ._builder import build_message_frame from ._checksum import compute as compute_checksum, validate as validate_checksum -from ._message import HarpMessage, HarpParseError, ParsedHarpMessage, parse as parse_message +from ._message import HarpMessage, HarpParseError, ParsedHarpMessage from ._message_type import ( MessageType, message_type_from_byte as message_type_from_byte, message_type_to_byte as message_type_to_byte, ) +from ._payload_converters import ( + Converter, + IdentityConverter, + StringConverter, + converter_registry, + register_converter, +) from ._payload import ( PayloadBase, PayloadU8, @@ -67,7 +74,12 @@ "HarpMessage", "ParsedHarpMessage", "HarpParseError", - "parse_message", + # Converters + "Converter", + "IdentityConverter", + "StringConverter", + "register_converter", + "converter_registry", # Payload DSL "PayloadBase", "PayloadU8", diff --git a/src/harp-protocol/harp/protocol/_message.py b/src/harp-protocol/harp/protocol/_message.py index be432e6..f89ff14 100644 --- a/src/harp-protocol/harp/protocol/_message.py +++ b/src/harp-protocol/harp/protocol/_message.py @@ -1,15 +1,14 @@ """Harp message container.""" import struct -from typing import Any, Generic, TypeVar, cast +from typing import Generic, TypeVar, cast from ._builder import build_message_frame from ._checksum import validate as _validate_checksum from ._message_type import MessageType, _message_type_from_byte_safe -from ._payload import PayloadBase from ._payload_type import PayloadType, decode_payload_type -P = TypeVar("P", bound=PayloadBase[Any]) +P = TypeVar("P") class HarpParseError(Exception): diff --git a/src/harp-protocol/harp/protocol/_payload.py b/src/harp-protocol/harp/protocol/_payload.py index 92ff3f1..4130e30 100644 --- a/src/harp-protocol/harp/protocol/_payload.py +++ b/src/harp-protocol/harp/protocol/_payload.py @@ -7,79 +7,24 @@ from numpy.typing import NDArray from typing_extensions import Self +from ._payload_converters import Converter as _Converter +from ._payload_converters import IdentityConverter as _IdentityConverter + NpStructT = TypeVar("NpStructT", bound=np.generic) T = TypeVar("T") E = TypeVar("E", bound=enum.IntEnum) -NpScalarT = TypeVar("NpScalarT", bound=np.generic) - - -# --------------------------------------------------------------------------- -# Converters -# --------------------------------------------------------------------------- - - -class _Converter(ABC, Generic[T]): - dtype: np.dtype - python_type: type - - @abstractmethod - def decode_scalar(self, view: Any) -> T: ... - - @abstractmethod - def decode_batch(self, view: Any) -> Any: ... - - @abstractmethod - def encode_into(self, view: Any, value: Any) -> None: ... - - -class _IdentityConverter(_Converter[NpScalarT]): - def __init__(self, dtype: "np.dtype[NpScalarT] | str | type[NpScalarT]") -> None: - self.dtype = np.dtype(dtype) - self.python_type = self.dtype.type - - def decode_scalar(self, view: Any) -> NpScalarT: - return view - - def decode_batch(self, view: Any) -> "NDArray[NpScalarT]": - return view - - def encode_into(self, view: Any, value: NpScalarT) -> None: - view[...] = value - - -class _StringConverter(_Converter[str]): - python_type = str - - def __init__(self, length: int, encoding: str = "ascii") -> None: - self._length = length - self._encoding = encoding - self.dtype = np.dtype((np.uint8, (length,))) - - def decode_scalar(self, view: Any) -> str: - return bytes(view).rstrip(b"\x00").decode(self._encoding) - - def decode_batch(self, view: Any) -> Any: - return np.array( - [bytes(row).rstrip(b"\x00").decode(self._encoding) for row in view], - dtype=object, - ) - - def encode_into(self, view: Any, value: str) -> None: - encoded = value.encode(self._encoding)[: self._length] - padded = encoded.ljust(self._length, b"\x00") - view[...] = np.frombuffer(padded, dtype=np.uint8) # --------------------------------------------------------------------------- # Descriptors — scalar variants (return Python / 0-D types) # --------------------------------------------------------------------------- -# A PayloadBase subclass declares fields with the scalar descriptors below. -# `__init_subclass__` auto-derives a Batch sibling whose descriptors are -# swapped to the matching ``*Batch`` counterpart via ``_to_batch()`` and -# return ``NDArray`` views instead. class _Field(Generic[T]): + """Descriptor for a payload field with a Converter. + Note: The name of the field is used as the slot name in the underlying numpy structured array. + """ + def __init__(self, converter: _Converter[T], *, name: str | None = None) -> None: self._converter = converter self._name = name @@ -102,6 +47,12 @@ def _to_batch(self) -> "_FieldBatch[T]": class _BitFlag: + """Descriptor for a bit flag within a payload field. + Notes: + 1) ensure dtype of the slot can hold the mask (e.g. uint8 for mask 0x01, uint16 for mask 0x100, etc.) + 2) An additional "slot" name can be provided if the bit flag is not stored in the same-named slot as the field descriptors (e.g. if multiple bit flags are packed into a single byte slot). + """ + def __init__( self, mask: int, @@ -127,6 +78,7 @@ def _to_batch(self) -> "_BitFlagBatch": def _build_enum_lookup(enum_cls: type) -> "tuple[list[str], np.ndarray]": + """Helper for _GroupMask to build the category list and code lookup table for a given enum.IntEnum class.""" members = list(enum_cls) categories = [m.name for m in members] max_val = max(int(m) for m in members) @@ -138,6 +90,12 @@ def _build_enum_lookup(enum_cls: type) -> "tuple[list[str], np.ndarray]": class _GroupMask(Generic[E]): + """Descriptor for a group of bits representing an enum value within a payload field. + Notes: + 1) ensure dtype of the slot can hold the mask (e.g. uint8 for mask 0x01, uint16 for mask 0x100, etc.) + 2) An additional "slot" name can be provided if the group mask is not stored in the same-named slot as the field descriptors (e.g. if multiple group masks are packed into a single byte slot). + """ + def __init__( self, mask: int, @@ -176,10 +134,13 @@ def _to_batch(self) -> "_GroupMaskBatch[E]": # --------------------------------------------------------------------------- # Descriptors — batch variants (return ndarray views) +# These are mostly used for batch operations like `to_dataframe` # --------------------------------------------------------------------------- class _FieldBatch(Generic[T]): + """Same as _Field but returns an NDArray view for batch payloads rather than a scalar value.""" + def __init__(self, converter: _Converter[T], *, name: str | None = None) -> None: self._converter = converter self._name = name @@ -199,6 +160,8 @@ def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: class _BitFlagBatch: + """Same as _BitFlag but returns an NDArray view for batch payloads rather than a scalar value.""" + def __init__( self, mask: int, @@ -221,6 +184,8 @@ def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: class _GroupMaskBatch(Generic[E]): + """Same as _GroupMask but returns an NDArray view for batch payloads rather than a scalar value.""" + def __init__( self, mask: int, @@ -248,10 +213,12 @@ def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: _PT = TypeVar("_PT", bound="PayloadBase[Any]") +_MISSING_INIT = object() -class Batch(Generic[_PT]): - """Phantom type for batched payloads. +class Batch(Generic[_PT], ABC): + """Alias type for batched payloads so we can have nice type-hinting + for batch operations like `read_frames` and `to_dataframe`. Statically, ``Batch[P]`` is a distinct type from ``P`` so the type checker knows ``read_frames`` returns an ndarray-shaped view rather @@ -266,14 +233,17 @@ class Batch(Generic[_PT]): raw_payload: "NDArray[Any]" value: "NDArray[Any]" + @abstractmethod def __len__(self) -> int: ... # type: ignore[empty-body] + @abstractmethod def to_dataframe(self, *, decode_enums: bool = True) -> "pd.DataFrame": ... # type: ignore[empty-body] + @abstractmethod def __getattr__(self, name: str) -> "NDArray[Any]": ... # type: ignore[empty-body] -# Tuples used by isinstance() checks throughout the module. +# Helpers for type checking using isinstance() _SCALAR_DECLARATION_TYPES = (_Field, _BitFlag, _GroupMask) _BATCH_DECLARATION_TYPES = (_FieldBatch, _BitFlagBatch, _GroupMaskBatch) _DECLARATION_TYPES = _SCALAR_DECLARATION_TYPES + _BATCH_DECLARATION_TYPES @@ -306,7 +276,7 @@ class PayloadBase(Generic[NpStructT]): buffers become Batch. """ - _dtype: ClassVar[np.dtype] + dtype: ClassVar[np.dtype] _repr_fields: ClassVar[tuple[str, ...]] _scalar_cls: ClassVar["type[PayloadBase]"] _batch_cls: ClassVar["type[PayloadBase]"] @@ -315,9 +285,10 @@ class PayloadBase(Generic[NpStructT]): def __init__(self, *args: object, **kwargs: object) -> None: cls = type(self) - names = self._dtype.names + names = self.dtype.names assert names is not None + # TODO this is just to allow; PayloadU16(foo) syntax. Not sure if it is worth it? if args and kwargs: raise TypeError( f"{cls.__name__}() does not accept positional and keyword args together" @@ -335,7 +306,7 @@ def __init__(self, *args: object, **kwargs: object) -> None: if unknown: raise TypeError(f"{cls.__name__}() got unexpected kwargs: {sorted(unknown)}") - arr = np.zeros((), dtype=self._dtype) + arr = np.zeros((), dtype=self.dtype) for name in names: if name not in slot_kwargs: @@ -388,7 +359,7 @@ def __init_subclass__( if _batch_of is not None: # Auto-generated Batch sibling: borrow dtype/_repr_fields from its # scalar twin and wire the scalar↔batch pointers. - cls._dtype = _batch_of._dtype + cls.dtype = _batch_of.dtype cls._repr_fields = _batch_of._repr_fields cls._scalar_cls = _batch_of cls._batch_cls = cls @@ -422,7 +393,7 @@ def __init_subclass__( ) else: slots[slot] = dtype - cls._dtype = np.dtype(list(slots.items())) + cls.dtype = np.dtype(list(slots.items())) if "_repr_fields" not in cls.__dict__: bitfield_names = tuple( @@ -431,7 +402,7 @@ def __init_subclass__( if bitfield_names: cls._repr_fields = bitfield_names else: - names = cls._dtype.names if hasattr(cls, "_dtype") else None + names = cls.dtype.names if hasattr(cls, "dtype") else None if names is not None and names != ("value",): cls._repr_fields = names else: @@ -440,7 +411,7 @@ def __init_subclass__( cls._scalar_cls = cls cls._batch_cls = cls # rebound below once Batch is generated - if hasattr(cls, "_dtype"): + if hasattr(cls, "dtype"): batch_attrs: dict[str, Any] = {"__init__": _batch_init_disabled} for name, val in cls.__dict__.items(): if isinstance(val, _SCALAR_DECLARATION_TYPES): @@ -461,16 +432,9 @@ def from_array(cls, arr: "np.ndarray") -> Self: @classmethod def from_buffer(cls, buf: bytes | bytearray | memoryview) -> Self: - arr = np.frombuffer(buf, dtype=cls._dtype) + arr = np.frombuffer(buf, dtype=cls.dtype) return cls.from_array(arr) - @property - def value(self) -> "NDArray[NpStructT]": - arr = self._arr - if arr.dtype.names == ("value",): - return arr["value"] - return arr - @property def raw_payload(self) -> NDArray[NpStructT]: return self._arr @@ -501,7 +465,7 @@ def to_dataframe(self, *, decode_enums: bool = True) -> pd.DataFrame: return pd.DataFrame(cols) cols = {} - names = self._dtype.names + names = self.dtype.names single_value_slot = names == ("value",) for name in names: desc = cls._mro_descriptor(name) @@ -512,7 +476,7 @@ def to_dataframe(self, *, decode_enums: bool = True) -> pd.DataFrame: cols[name] = np.atleast_1d(getattr(self, name)) continue - field_dtype, _ = self._dtype.fields[name] + field_dtype, _ = self.dtype.fields[name] sub = arr[name] if field_dtype.subdtype is None: cols[name] = sub @@ -537,83 +501,166 @@ def __repr__(self) -> str: def __str__(self) -> str: return repr(self) + @classmethod + def unwrap(cls, arr: "np.ndarray") -> Any: + """Dispatch hook used by ``RegisterBase.parse``. + + Struct payloads return a typed wrapper so descriptors like + ``payload.Channel0`` work. Anonymous payloads override this to + return the raw numpy scalar/ndarray directly. + """ + return cls.from_array(arr) -class PayloadU8(PayloadBase[np.uint8]): - value = _Field(_IdentityConverter(np.dtype("u1"))) +# --------------------------------------------------------------------------- +# AnonymousPayload — single unnamed slot, no user-facing descriptors +# --------------------------------------------------------------------------- -class PayloadU16(PayloadBase[np.uint16]): - value = _Field(_IdentityConverter(np.dtype(" None: + if scalar_dtype is not None: + cls.dtype = np.dtype(scalar_dtype) + cls._repr_fields = () + super().__init_subclass__(**kwargs) + def __init__(self, value: object = _MISSING_INIT, /, **kwargs: object) -> None: # type: ignore[override] + if value is _MISSING_INIT: + if "value" in kwargs: + value = kwargs.pop("value") + else: + raise TypeError(f"{type(self).__name__}() requires a value") + if kwargs: + raise TypeError(f"{type(self).__name__}() got unexpected kwargs: {sorted(kwargs)}") + self._arr = np.asarray(value, dtype=self.dtype) -class PayloadS16(PayloadBase[np.int16]): - value = _Field(_IdentityConverter(np.dtype(" Any: + # 0-D → numpy scalar via item-like access (preserves dtype). + # 1-D / sub-array → return the ndarray as-is. + return arr if arr.ndim > 0 else arr[()] + + def _repr_kwargs(self) -> str: + return repr(self._arr.tolist() if self._arr.ndim > 0 else self._arr[()]) + + def __repr__(self) -> str: + return f"{type(self).__name__}({self._repr_kwargs()})" + def to_dataframe(self, *, decode_enums: bool = True) -> pd.DataFrame: + arr = np.atleast_1d(self._arr) + # Sub-array dtype (array register): shape is already (N, length). + if arr.ndim > 1: + return pd.DataFrame({str(i): arr[:, i] for i in range(arr.shape[1])}) + return pd.DataFrame({"value": arr}) -class PayloadS32(PayloadBase[np.int32]): - value = _Field(_IdentityConverter(np.dtype(" "Callable[[type[Converter[Any]]], type[Converter[Any]]]": + """Class decorator that registers a ``Converter`` subclass under ``name``. + + Raises ``ValueError`` if the name is already registered. + + Usage:: + + @register_converter(name="my_converter") + class MyConverter(Converter[int]): ... + """ + + def _register(cls: "type[Converter[Any]]") -> "type[Converter[Any]]": + if name in converter_registry: + raise ValueError( + f"A converter named {name!r} is already registered " + f"(existing: {converter_registry[name]!r}, new: {cls!r})" + ) + converter_registry[name] = cls + return cls + + return _register + + +# --------------------------------------------------------------------------- +# Base class +# --------------------------------------------------------------------------- + + +class Converter(ABC, Generic[T]): + """Abstract base for payload field converters. + + Subclasses must set ``dtype`` and ``init_kwarg_type`` as class attributes + and implement the three abstract methods. + """ + + dtype: np.dtype # dtype of the raw numpy slot passed to decode/encode + init_kwarg_type: type # used for docs/introspection; not enforced at runtime + + @abstractmethod + def decode_scalar(self, view: np.generic) -> T: + """Decode a 0-D structured-array element into a Python value.""" + + @abstractmethod + def decode_batch(self, view: "NDArray[np.generic]") -> Any: + """Decode a 1-D structured-array column into an array-like.""" + + @abstractmethod + def encode_into(self, view: NDArray[np.generic], value: T) -> None: + """Write a Python value back into a structured-array element.""" + + +# --------------------------------------------------------------------------- +# Built-in converters +# --------------------------------------------------------------------------- + + +class IdentityConverter(Converter[NpScalarT]): + """Pass-through converter — the raw numpy scalar is returned as-is.""" + + def __init__(self, dtype: "np.dtype[NpScalarT] | str | type[NpScalarT]") -> None: + self.dtype = np.dtype(dtype) + self.init_kwarg_type = self.dtype.type + + def decode_scalar(self, view: np.generic) -> NpScalarT: + return cast( + NpScalarT, view + ) # this should be safe since dtype is scalar and matches the type var + + def decode_batch(self, view: NDArray[np.generic]) -> "NDArray[NpScalarT]": + return cast("NDArray[NpScalarT]", view) + + def encode_into(self, view: NDArray[np.generic], value: NpScalarT) -> None: + view[...] = value + + +@register_converter(name="String") # TODO check this name against Bonsai +class StringConverter(Converter[str]): + """Converts a fixed-length byte array to/from a Python ``str``.""" + + init_kwarg_type = str + + def __init__(self, length: int, encoding: str = "ascii") -> None: + self._length = length + self._encoding = encoding + self.dtype = np.dtype((np.uint8, (length,))) + + def decode_scalar(self, view: np.generic) -> str: + return bytes(view).rstrip(b"\x00").decode(self._encoding) + + def decode_batch(self, view: NDArray[np.generic]) -> Any: + return np.array( + [bytes(row).rstrip(b"\x00").decode(self._encoding) for row in view], + dtype=object, + ) + + def encode_into(self, view: NDArray[np.generic], value: str) -> None: + encoded = value.encode(self._encoding)[: self._length] + padded = encoded.ljust(self._length, b"\x00") + view[...] = np.frombuffer(padded, dtype=np.uint8) diff --git a/src/harp-protocol/harp/protocol/_register.py b/src/harp-protocol/harp/protocol/_register.py index 5b3a52f..bca7703 100644 --- a/src/harp-protocol/harp/protocol/_register.py +++ b/src/harp-protocol/harp/protocol/_register.py @@ -1,9 +1,9 @@ from abc import ABC, ABCMeta -from pathlib import Path from typing import Any, ClassVar, Generic, TypeVar, cast, final, overload import numpy as np import pandas as pd +from numpy.typing import NDArray from typing_extensions import Sentinel from ._builder import build_message_frame @@ -35,7 +35,7 @@ _MISSING = Sentinel("_MISSING") -P = TypeVar("P", bound=PayloadBase[Any]) +U = TypeVar("U") _R = TypeVar("_R") _AR = TypeVar("_AR", bound="RegisterBase[Any]") @@ -50,11 +50,17 @@ def __call__(cls: "type[_R]", address: int) -> "type[_R]": ) -class RegisterBase(ABC, Generic[P]): +class RegisterBase(ABC, Generic[U]): """Abstract base for all typed Harp registers. - Subclasses must define ``address`` and ``payload_type`` as ``ClassVar``s. - Optionally define ``payload_class`` for structured payloads; otherwise a scalar one is generated. + The generic parameter ``U`` is the static return type of :meth:`parse`: + + * scalar registers → a numpy scalar (e.g. ``np.uint16``); + * array registers → ``NDArray[…]`` of fixed length; + * structured registers → the payload class itself. + + Subclasses must define ``address``, ``payload_type``, and + ``payload_class`` as ``ClassVar``s. """ address: ClassVar[int] @@ -63,11 +69,16 @@ class RegisterBase(ABC, Generic[P]): length: ClassVar[int | None] = None @classmethod - def parse(cls, value: HarpMessage | bytes | bytearray | memoryview) -> P: - """Parse a single message into a 0-D payload. It will only try to parse the first structured record if the payload is an array.""" + def parse(cls, value: HarpMessage | bytes | bytearray | memoryview) -> U: + """Parse a single message into the user-facing payload value. + + Struct payloads return a typed wrapper (descriptor access like + ``payload.Channel0`` works). Anonymous payloads (scalar / array + registers) return the raw numpy scalar or ndarray directly. + """ buf = value.payload if isinstance(value, HarpMessage) else value record = np.frombuffer(buf, dtype=cls.payload_class.dtype, count=1)[0] - return cast(P, cls.payload_class.from_array(record)) + return cast(U, cls.payload_class.unwrap(record)) @classmethod def parse_bulk( @@ -75,7 +86,7 @@ def parse_bulk( source: bytes | bytearray | memoryview, *, parse_timestamp: bool = True, - ) -> "tuple[np.ndarray, np.ndarray | None, np.ndarray | None, Batch[P]]": + ) -> "tuple[np.ndarray, np.ndarray | None, np.ndarray | None, Batch[Any]]": """Parse a bulk buffer containing one or more frames of this register type. Returns (data, timestamps, msgtype_view, payload).""" # Returns (data, timestamps, msgtype_view, payload). ``data`` is payload_cls = cls.payload_class @@ -84,7 +95,7 @@ def parse_bulk( if len(data) == 0: # No frames, but still need to return a Batch with the right dtype. payload = payload_cls.from_array(np.empty(0, dtype=payload_cls.dtype)) - return data, None, None, cast("Batch[P]", payload) + return data, None, None, cast("Batch[Any]", payload) stride = ( int(data[1]) + 2 @@ -112,7 +123,7 @@ def parse_bulk( ) payload = payload_cls.from_array(payload_arr) - return data, timestamps, msgtype_view, cast("Batch[P]", payload) + return data, timestamps, msgtype_view, cast("Batch[Any]", payload) @classmethod def read_dataframe( @@ -194,64 +205,64 @@ def format( ) -class RegisterU8(RegisterBase[PayloadU8], metaclass=_RegisterMeta): - """A simple scalar register with a uint8 payload.""" +class RegisterU8(RegisterBase[np.uint8], metaclass=_RegisterMeta): + """A simple scalar register with a uint8 payload. ``parse()`` returns ``np.uint8``.""" payload_type: ClassVar[PayloadType] = PayloadType.U8 payload_class = PayloadU8 -class RegisterU16(RegisterBase[PayloadU16], metaclass=_RegisterMeta): - """A simple scalar register with a uint16 payload.""" +class RegisterU16(RegisterBase[np.uint16], metaclass=_RegisterMeta): + """A simple scalar register with a uint16 payload. ``parse()`` returns ``np.uint16``.""" payload_type: ClassVar[PayloadType] = PayloadType.U16 payload_class = PayloadU16 -class RegisterU32(RegisterBase[PayloadU32], metaclass=_RegisterMeta): - """A simple scalar register with a uint32 payload.""" +class RegisterU32(RegisterBase[np.uint32], metaclass=_RegisterMeta): + """A simple scalar register with a uint32 payload. ``parse()`` returns ``np.uint32``.""" payload_type: ClassVar[PayloadType] = PayloadType.U32 payload_class = PayloadU32 -class RegisterU64(RegisterBase[PayloadU64], metaclass=_RegisterMeta): - """A simple scalar register with a uint64 payload.""" +class RegisterU64(RegisterBase[np.uint64], metaclass=_RegisterMeta): + """A simple scalar register with a uint64 payload. ``parse()`` returns ``np.uint64``.""" payload_type: ClassVar[PayloadType] = PayloadType.U64 payload_class = PayloadU64 -class RegisterS8(RegisterBase[PayloadS8], metaclass=_RegisterMeta): - """A simple scalar register with a int8 payload.""" +class RegisterS8(RegisterBase[np.int8], metaclass=_RegisterMeta): + """A simple scalar register with a int8 payload. ``parse()`` returns ``np.int8``.""" payload_type: ClassVar[PayloadType] = PayloadType.S8 payload_class = PayloadS8 -class RegisterS16(RegisterBase[PayloadS16], metaclass=_RegisterMeta): - """A simple scalar register with a int16 payload.""" +class RegisterS16(RegisterBase[np.int16], metaclass=_RegisterMeta): + """A simple scalar register with a int16 payload. ``parse()`` returns ``np.int16``.""" payload_type: ClassVar[PayloadType] = PayloadType.S16 payload_class = PayloadS16 -class RegisterS32(RegisterBase[PayloadS32], metaclass=_RegisterMeta): - """A simple scalar register with a int32 payload.""" +class RegisterS32(RegisterBase[np.int32], metaclass=_RegisterMeta): + """A simple scalar register with a int32 payload. ``parse()`` returns ``np.int32``.""" payload_type: ClassVar[PayloadType] = PayloadType.S32 payload_class = PayloadS32 -class RegisterS64(RegisterBase[PayloadS64], metaclass=_RegisterMeta): - """A simple scalar register with a int64 payload.""" +class RegisterS64(RegisterBase[np.int64], metaclass=_RegisterMeta): + """A simple scalar register with a int64 payload. ``parse()`` returns ``np.int64``.""" payload_type: ClassVar[PayloadType] = PayloadType.S64 payload_class = PayloadS64 -class RegisterFloat(RegisterBase[PayloadFloat], metaclass=_RegisterMeta): - """A simple scalar register with a float32 payload.""" +class RegisterFloat(RegisterBase[np.float32], metaclass=_RegisterMeta): + """A simple scalar register with a float32 payload. ``parse()`` returns ``np.float32``.""" payload_type: ClassVar[PayloadType] = PayloadType.Float payload_class = PayloadFloat @@ -261,15 +272,16 @@ class _ArrayRegisterMeta(ABCMeta): """A base metaclass for array registers. Calling with address and length creates a concrete subclass: ``RegisterU16Array(0x28, length=3)``.""" def __call__(cls: "type[_AR]", address: int, *, length: int) -> "type[_AR]": # type: ignore[override, misc] - from ._payload import _Field, _IdentityConverter - base_payload = cls.payload_class # type: ignore[attr-defined] - inner = base_payload.dtype.fields["value"][0] + # Anonymous payloads carry a plain (non-structured) dtype. The array + # variant uses a sub-dtype (inner_dtype, (length,)) so a single buffer + # element decodes directly to an ndarray of shape (length,). + inner = base_payload.dtype sub_dtype = np.dtype((inner, (length,))) concrete_payload = type( f"{base_payload.__name__}_{length}", (base_payload,), - {"value": _Field(_IdentityConverter(sub_dtype), name="value")}, # we + {"dtype": sub_dtype}, ) return cast( "type[_AR]", @@ -285,64 +297,64 @@ def __call__(cls: "type[_AR]", address: int, *, length: int) -> "type[_AR]": # ) -class RegisterU8Array(RegisterBase[PayloadU8Array], metaclass=_ArrayRegisterMeta): - """A simple array register with a uint8 array payload. It must be instantiated with a length: ``RegisterU8Array(0x28, length=3)``.""" +class RegisterU8Array(RegisterBase[NDArray[np.uint8]], metaclass=_ArrayRegisterMeta): + """A simple array register with a uint8 array payload. It must be instantiated with a length: ``RegisterU8Array(0x28, length=3)``. ``parse()`` returns ``NDArray[np.uint8]``.""" payload_type: ClassVar[PayloadType] = PayloadType.U8 payload_class = PayloadU8Array -class RegisterU16Array(RegisterBase[PayloadU16Array], metaclass=_ArrayRegisterMeta): - """A simple array register with a uint16 array payload. It must be instantiated with a length: ``RegisterU16Array(0x28, length=3)``.""" +class RegisterU16Array(RegisterBase[NDArray[np.uint16]], metaclass=_ArrayRegisterMeta): + """A simple array register with a uint16 array payload. It must be instantiated with a length: ``RegisterU16Array(0x28, length=3)``. ``parse()`` returns ``NDArray[np.uint16]``.""" payload_type: ClassVar[PayloadType] = PayloadType.U16 payload_class = PayloadU16Array -class RegisterU32Array(RegisterBase[PayloadU32Array], metaclass=_ArrayRegisterMeta): - """A simple array register with a uint32 array payload. It must be instantiated with a length: ``RegisterU32Array(0x28, length=3)``.""" +class RegisterU32Array(RegisterBase[NDArray[np.uint32]], metaclass=_ArrayRegisterMeta): + """A simple array register with a uint32 array payload. It must be instantiated with a length: ``RegisterU32Array(0x28, length=3)``. ``parse()`` returns ``NDArray[np.uint32]``.""" payload_type: ClassVar[PayloadType] = PayloadType.U32 payload_class = PayloadU32Array -class RegisterU64Array(RegisterBase[PayloadU64Array], metaclass=_ArrayRegisterMeta): - """A simple array register with a uint64 array payload. It must be instantiated with a length: ``RegisterU64Array(0x28, length=3)``.""" +class RegisterU64Array(RegisterBase[NDArray[np.uint64]], metaclass=_ArrayRegisterMeta): + """A simple array register with a uint64 array payload. It must be instantiated with a length: ``RegisterU64Array(0x28, length=3)``. ``parse()`` returns ``NDArray[np.uint64]``.""" payload_type: ClassVar[PayloadType] = PayloadType.U64 payload_class = PayloadU64Array -class RegisterS8Array(RegisterBase[PayloadS8Array], metaclass=_ArrayRegisterMeta): - """A simple array register with a int8 array payload. It must be instantiated with a length: ``RegisterS8Array(0x28, length=3)``.""" +class RegisterS8Array(RegisterBase[NDArray[np.int8]], metaclass=_ArrayRegisterMeta): + """A simple array register with a int8 array payload. It must be instantiated with a length: ``RegisterS8Array(0x28, length=3)``. ``parse()`` returns ``NDArray[np.int8]``.""" payload_type: ClassVar[PayloadType] = PayloadType.S8 payload_class = PayloadS8Array -class RegisterS16Array(RegisterBase[PayloadS16Array], metaclass=_ArrayRegisterMeta): - """A simple array register with a int16 array payload. It must be instantiated with a length: ``RegisterS16Array(0x28, length=3)``.""" +class RegisterS16Array(RegisterBase[NDArray[np.int16]], metaclass=_ArrayRegisterMeta): + """A simple array register with a int16 array payload. It must be instantiated with a length: ``RegisterS16Array(0x28, length=3)``. ``parse()`` returns ``NDArray[np.int16]``.""" payload_type: ClassVar[PayloadType] = PayloadType.S16 payload_class = PayloadS16Array -class RegisterS32Array(RegisterBase[PayloadS32Array], metaclass=_ArrayRegisterMeta): - """A simple array register with a int32 array payload. It must be instantiated with a length: ``RegisterS32Array(0x28, length=3)``.""" +class RegisterS32Array(RegisterBase[NDArray[np.int32]], metaclass=_ArrayRegisterMeta): + """A simple array register with a int32 array payload. It must be instantiated with a length: ``RegisterS32Array(0x28, length=3)``. ``parse()`` returns ``NDArray[np.int32]``.""" payload_type: ClassVar[PayloadType] = PayloadType.S32 payload_class = PayloadS32Array -class RegisterS64Array(RegisterBase[PayloadS64Array], metaclass=_ArrayRegisterMeta): - """A simple array register with a int64 array payload. It must be instantiated with a length: ``RegisterS64Array(0x28, length=3)``.""" +class RegisterS64Array(RegisterBase[NDArray[np.int64]], metaclass=_ArrayRegisterMeta): + """A simple array register with a int64 array payload. It must be instantiated with a length: ``RegisterS64Array(0x28, length=3)``. ``parse()`` returns ``NDArray[np.int64]``.""" payload_type: ClassVar[PayloadType] = PayloadType.S64 payload_class = PayloadS64Array -class RegisterFloatArray(RegisterBase[PayloadFloatArray], metaclass=_ArrayRegisterMeta): - """A simple array register with a float32 array payload. It must be instantiated with a length: ``RegisterFloatArray(0x28, length=3)``.""" +class RegisterFloatArray(RegisterBase[NDArray[np.float32]], metaclass=_ArrayRegisterMeta): + """A simple array register with a float32 array payload. It must be instantiated with a length: ``RegisterFloatArray(0x28, length=3)``. ``parse()`` returns ``NDArray[np.float32]``.""" payload_type: ClassVar[PayloadType] = PayloadType.Float payload_class = PayloadFloatArray diff --git a/tests/protocol/test_converter.py b/tests/protocol/test_converter.py index 90b1a43..4464217 100644 --- a/tests/protocol/test_converter.py +++ b/tests/protocol/test_converter.py @@ -44,14 +44,14 @@ class _NumericPayload(PayloadBase): def test_identity_converter_scalar_view(): - rec = np.array((-7, 99), dtype=_NumericPayload._dtype) + rec = np.array((-7, 99), dtype=_NumericPayload.dtype) p = _NumericPayload.from_array(rec) assert int(p.a) == -7 assert int(p.b) == 99 def test_identity_converter_batch_view(): - arr = np.array([(-7, 99), (1, 2)], dtype=_NumericPayload._dtype) + arr = np.array([(-7, 99), (1, 2)], dtype=_NumericPayload.dtype) p = _NumericPayload.from_buffer(arr.tobytes()) np.testing.assert_array_equal(p.a, [-7, 1]) np.testing.assert_array_equal(p.b, [99, 2]) @@ -69,9 +69,9 @@ class DeclaredPayload(PayloadBase): def test_declared_dtype_synthesised_from_fields(): # Field declarations alone build a structured dtype in declaration order. - assert DeclaredPayload._dtype.names == ("delta", "flag") - assert DeclaredPayload._dtype.fields["delta"][0] == np.dtype(np.uint32) - assert DeclaredPayload._dtype.fields["flag"][0] == np.dtype(np.uint8) + assert DeclaredPayload.dtype.names == ("delta", "flag") + assert DeclaredPayload.dtype.fields["delta"][0] == np.dtype(np.uint32) + assert DeclaredPayload.dtype.fields["flag"][0] == np.dtype(np.uint8) def test_declared_dtype_kwarg_init_round_trip(): @@ -96,8 +96,8 @@ class _NamedPayload(PayloadBase): def test_string_converter_dtype_synthesis(): # name should occupy 8 bytes, delta 2 bytes. - assert _NamedPayload._dtype.itemsize == 10 - assert _NamedPayload._dtype.fields["name"][0].subdtype is not None + assert _NamedPayload.dtype.itemsize == 10 + assert _NamedPayload.dtype.fields["name"][0].subdtype is not None def test_string_converter_scalar_decode_roundtrip(): @@ -190,7 +190,7 @@ class _Flags(PayloadBase): group = _GroupMask(0x06, 1, _Color, dtype=np.uint8) # 0-D scalar record: flag=1, group bits=01 (Green) - scalar = _Flags.from_array(np.array((0x03,), dtype=_Flags._dtype)) + scalar = _Flags.from_array(np.array((0x03,), dtype=_Flags.dtype)) assert type(scalar) is _Flags assert scalar.flag is True assert scalar.group is _Color.Green diff --git a/tests/protocol/test_message.py b/tests/protocol/test_message.py index 8e650af..c22465f 100644 --- a/tests/protocol/test_message.py +++ b/tests/protocol/test_message.py @@ -2,7 +2,7 @@ import numpy as np import pytest -from harp.protocol._message import HarpParseError, parse +from harp.protocol._message import HarpMessage, HarpParseError from harp.protocol._message_type import MessageType from harp.protocol._payload_type import PayloadType @@ -12,7 +12,7 @@ def test_parse_read_request(): """Read request: no payload, no timestamp.""" frame = make_frame_from_raw(0x01, address=8, port=0xFF, payload_type=0x04, payload=b"") - msg = parse(frame) + msg = HarpMessage.parse(frame) assert msg.message_type == MessageType.Read assert msg.has_error is False assert msg.address == 8 @@ -23,7 +23,7 @@ def test_parse_read_request(): def test_parse_write_u8_payload(): frame = make_frame_from_raw(0x02, address=10, port=0xFF, payload_type=0x01, payload=b"\x05") - msg = parse(frame) + msg = HarpMessage.parse(frame) assert msg.message_type == MessageType.Write assert msg.payload == b"\x05" assert msg.payload_type == PayloadType.U8 @@ -38,7 +38,7 @@ def test_parse_with_timestamp(): payload=b"\x7f", timestamp=TIMESTAMP_1S, ) - msg = parse(frame) + msg = HarpMessage.parse(frame) assert msg.message_type == MessageType.Event assert msg.timestamp == pytest.approx(1.0) assert msg.payload == b"\x7f" @@ -46,7 +46,7 @@ def test_parse_with_timestamp(): def test_parse_error_flag(): frame = make_frame_from_raw(0x09, address=0, port=0xFF, payload_type=0x01, payload=b"\x00") - msg = parse(frame) + msg = HarpMessage.parse(frame) assert msg.has_error is True assert msg.message_type == MessageType.Read @@ -54,7 +54,7 @@ def test_parse_error_flag(): def test_parse_u16_array(): payload = struct.pack(" pd.DataFrame: def _make_simple_bytes(n: int) -> bytes: - arr = np.zeros(n, dtype=SimplePayload._dtype) + arr = np.zeros(n, dtype=SimplePayload.dtype) arr["x"] = np.arange(n, dtype=np.int16) * -1 arr["y"] = np.arange(n, dtype=np.uint8) return arr.tobytes() @@ -48,7 +49,7 @@ def test_to_dataframe_columns(): def test_to_dataframe_override(): - arr = np.array([(0b00000011,), (0b00000001,), (0b00000010,)], dtype=BitPackedPayload._dtype) + arr = np.array([(0b00000011,), (0b00000001,), (0b00000010,)], dtype=BitPackedPayload.dtype) p = BitPackedPayload.from_buffer(arr.tobytes()) df = p.to_dataframe() assert list(df.columns) == ["flag_a", "flag_b"] @@ -66,4 +67,4 @@ def test_from_buffer_zero_copy(): def test_payload_property(): p = SimplePayload.from_buffer(_make_simple_bytes(2)) - assert p.raw_payload.dtype == SimplePayload._dtype + assert p.raw_payload.dtype == SimplePayload.dtype diff --git a/tests/protocol/test_register.py b/tests/protocol/test_register.py index cbb6fff..ca7ca94 100644 --- a/tests/protocol/test_register.py +++ b/tests/protocol/test_register.py @@ -120,10 +120,10 @@ def test_named_register_roundtrip(value): frame = TimestampSecond.format(value) msg = _parse_frame(frame) parsed = TimestampSecond.parse(msg) - assert isinstance(parsed, PayloadU32) - # parse() returns a 0-D scalar; compare to the Python value directly. - assert parsed.value == value - assert parsed.value.ndim == 0 + # Anonymous-payload registers unwrap to a numpy scalar. + assert isinstance(parsed, np.uint32) + assert parsed == value + assert parsed.ndim == 0 def test_named_register_format_read_frame(): @@ -144,9 +144,9 @@ def test_factory_format_and_parse(): frame = reg.format(100) msg = _parse_frame(frame) parsed = reg.parse(msg) - assert isinstance(parsed, PayloadU32) - assert parsed.value == 100 - assert parsed.value.ndim == 0 + assert isinstance(parsed, np.uint32) + assert parsed == 100 + assert parsed.ndim == 0 def test_factory_different_addresses_are_independent(): @@ -182,11 +182,11 @@ def test_format_with_payload_instance_via_register(): frame = TimestampSecond.format(payload) msg = _parse_frame(frame) parsed = TimestampSecond.parse(msg) - assert parsed.value == 42 + assert parsed == 42 def test_structured_register_format_single_sample(): - sample = np.array([(100, 512, -200)], dtype=AnalogDataPayload._dtype) + sample = np.array([(100, 512, -200)], dtype=AnalogDataPayload.dtype) frame = AnalogData.format(sample) msg = _parse_frame(frame) parsed = AnalogData.parse(msg) @@ -200,7 +200,7 @@ def test_structured_register_format_single_sample(): def test_structured_register_to_dataframe(): raw = np.array( [(1, 2, 3), (4, 5, 6)], - dtype=AnalogDataPayload._dtype, + dtype=AnalogDataPayload.dtype, ).tobytes() # Bulk decode goes through .Batch; from_buffer handles the redirect. bulk = AnalogDataPayload.from_buffer(raw) @@ -240,9 +240,9 @@ def test_array_register_parse_roundtrip(): frame = reg.format(values) msg = _parse_frame(frame) parsed = reg.parse(msg) - # parse() yields a 0-D record holding one length-3 sub-array. - np.testing.assert_array_equal(parsed.value, values) - assert parsed.value.shape == (3,) + # parse() yields a 1-D length-N ndarray directly. + np.testing.assert_array_equal(parsed, values) + assert parsed.shape == (3,) def test_s16_array_roundtrip(): @@ -251,8 +251,8 @@ def test_s16_array_roundtrip(): frame = reg.format(values) msg = _parse_frame(frame) parsed = reg.parse(msg) - np.testing.assert_array_equal(parsed.value, values) - assert parsed.value.shape == (4,) + np.testing.assert_array_equal(parsed, values) + assert parsed.shape == (4,) def test_unnamed_register_auto_payload_class(): @@ -264,7 +264,7 @@ class MyReg(RegisterU8): # payload_class should exist and parse correctly raw = np.array([7], dtype=np.dtype("u1")).tobytes() parsed = MyReg.parse(raw) - assert parsed.value == 7 + assert parsed == 7 def test_explicit_payload_class_not_overwritten(): @@ -302,11 +302,11 @@ def test_format_write_with_timestamp(): assert msg.has_timestamp assert msg.timestamp == pytest.approx(ts, abs=1e-4) parsed = TimestampSecond.parse(msg) - assert parsed.value == 42 + assert parsed == 42 # --------------------------------------------------------------------------- -# 9. .value property +# 9. AnonymousPayload / register parse return contract # --------------------------------------------------------------------------- @@ -324,84 +324,42 @@ def test_format_write_with_timestamp(): (PayloadFloat, 1.5, np.dtype(" Date: Sat, 16 May 2026 20:09:41 -0700 Subject: [PATCH 212/267] Add example of heterogenous register --- scripts/heterogenous_register.py | 87 +++++++++++++++++++ .../harp/protocol/_payload_converters.py | 63 +++++++++++++- 2 files changed, 146 insertions(+), 4 deletions(-) create mode 100644 scripts/heterogenous_register.py diff --git a/scripts/heterogenous_register.py b/scripts/heterogenous_register.py new file mode 100644 index 0000000..75ac8f0 --- /dev/null +++ b/scripts/heterogenous_register.py @@ -0,0 +1,87 @@ +from typing import ClassVar + +import numpy as np + +from harp.protocol._message import HarpMessage +from harp.protocol._message_type import MessageType +from harp.protocol._payload import PayloadBase, _Field +from harp.protocol._payload_converters import StringConverter, UInt32Converter +from harp.protocol._payload_type import PayloadType +from harp.protocol._register import RegisterBase + +# --------------------------------------------------------------------------- +# Payload +# --------------------------------------------------------------------------- + +# FileSettings0: +# address: 54 +# type: U8 +# access: Write +# length: 45 +# description: "Struct to configure Analog Output Channel 0 +# File Player settings: cycles (U32), duration_us (U32), +# update_frequency_hz (U32), path (U8 array, 33 elements)" + + +class FileSettings0Payload(PayloadBase[np.uint8]): # np.uint8 is the word size + """_summary_ + + Args: + PayloadBase (_type_): _description_ + """ + + cycles = _Field(UInt32Converter()) + duration_us = _Field(UInt32Converter()) + update_frequency_hz = _Field(UInt32Converter()) + path = _Field(StringConverter(33)) + + +# --------------------------------------------------------------------------- +# Register +# --------------------------------------------------------------------------- + + +class FileSettings0(RegisterBase[FileSettings0Payload]): + address: ClassVar[int] = 54 + payload_type: ClassVar[PayloadType] = PayloadType.U8 + payload_class: ClassVar[type[PayloadBase]] = FileSettings0Payload + + +# --------------------------------------------------------------------------- +# Round-trip demo +# --------------------------------------------------------------------------- + +if __name__ == "__main__": + # 1. Build payload + payload = FileSettings0Payload( # The stubs for the constructor must be auto generated, but this already works + cycles=3, + duration_us=250_000, + update_frequency_hz=400, + path="220khzwaveform", + ) + print(f"Payload dtype : {FileSettings0Payload.dtype}") + print(f"Payload bytes : {payload.raw_payload.tobytes().hex()}") + print(f"Payload : {payload}") + + # 2. Encode → Harp wire frame + frame = FileSettings0.format(payload, message_type=MessageType.Write) + print(f"\nWire frame ({len(frame)} bytes): {frame.hex()}") + + # 3. Parse wire frame → HarpMessage + msg = HarpMessage.parse(frame) + print(f"\nHarpMessage : {msg}") + + # 4. Parse payload from HarpMessage + parsed = FileSettings0.parse(msg) + print(f"\nParsed payload : {parsed}") + print(f" cycles = {parsed.cycles}") + print(f" duration_us = {parsed.duration_us}") + print(f" update_frequency_hz = {parsed.update_frequency_hz}") + print(f" path = {parsed.path!r}") + + # 5. Assert round-trip integrity + assert parsed.cycles == 3 + assert parsed.duration_us == 250_000 + assert parsed.update_frequency_hz == 400 + assert parsed.path == "220khzwaveform" + print("\nRound-trip OK") diff --git a/src/harp-protocol/harp/protocol/_payload_converters.py b/src/harp-protocol/harp/protocol/_payload_converters.py index 3951eb2..bfd2bbc 100644 --- a/src/harp-protocol/harp/protocol/_payload_converters.py +++ b/src/harp-protocol/harp/protocol/_payload_converters.py @@ -7,6 +7,7 @@ T = TypeVar("T") NpScalarT = TypeVar("NpScalarT", bound=np.generic) +_ConverterClsT = TypeVar("_ConverterClsT", bound="type[Converter[Any]]") # --------------------------------------------------------------------------- # Registry @@ -15,7 +16,7 @@ converter_registry: "dict[str, type[Converter[Any]]]" = {} -def register_converter(*, name: str) -> "Callable[[type[Converter[Any]]], type[Converter[Any]]]": +def register_converter(*, name: str) -> "Callable[[_ConverterClsT], _ConverterClsT]": """Class decorator that registers a ``Converter`` subclass under ``name``. Raises ``ValueError`` if the name is already registered. @@ -26,7 +27,7 @@ def register_converter(*, name: str) -> "Callable[[type[Converter[Any]]], type[C class MyConverter(Converter[int]): ... """ - def _register(cls: "type[Converter[Any]]") -> "type[Converter[Any]]": + def _register(cls: "_ConverterClsT") -> "_ConverterClsT": if name in converter_registry: raise ValueError( f"A converter named {name!r} is already registered " @@ -51,7 +52,7 @@ class Converter(ABC, Generic[T]): """ dtype: np.dtype # dtype of the raw numpy slot passed to decode/encode - init_kwarg_type: type # used for docs/introspection; not enforced at runtime + init_kwarg_type: type # used for docs/introspection TODO especially to generate the constructor type hints; not enforced at runtime @abstractmethod def decode_scalar(self, view: np.generic) -> T: @@ -90,7 +91,61 @@ def encode_into(self, view: NDArray[np.generic], value: NpScalarT) -> None: view[...] = value -@register_converter(name="String") # TODO check this name against Bonsai +@register_converter(name="byte") +class UInt8Converter(IdentityConverter[np.uint8]): + def __init__(self) -> None: + super().__init__(np.uint8) + + +@register_converter(name="sbyte") +class SInt8Converter(IdentityConverter[np.int8]): + def __init__(self) -> None: + super().__init__(np.int8) + + +@register_converter(name="ushort") +class UInt16Converter(IdentityConverter[np.uint16]): + def __init__(self) -> None: + super().__init__(np.uint16) + + +@register_converter(name="short") +class Int16Converter(IdentityConverter[np.int16]): + def __init__(self) -> None: + super().__init__(np.int16) + + +@register_converter(name="uint") +class UInt32Converter(IdentityConverter[np.uint32]): + def __init__(self) -> None: + super().__init__(np.uint32) + + +@register_converter(name="int") +class Int32Converter(IdentityConverter[np.int32]): + def __init__(self) -> None: + super().__init__(np.int32) + + +@register_converter(name="ulong") +class UInt64Converter(IdentityConverter[np.uint64]): + def __init__(self) -> None: + super().__init__(np.uint64) + + +@register_converter(name="long") +class Int64Converter(IdentityConverter[np.int64]): + def __init__(self) -> None: + super().__init__(np.int64) + + +@register_converter(name="float") +class FloatConverter(IdentityConverter[np.float32]): + def __init__(self) -> None: + super().__init__(np.float32) + + +@register_converter(name="string") # TODO check this name against Bonsai class StringConverter(Converter[str]): """Converts a fixed-length byte array to/from a Python ``str``.""" From 00bf9c7bf9b70267c454dac84e50888ae5d482c0 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sat, 16 May 2026 21:45:41 -0700 Subject: [PATCH 213/267] Raise proper error --- src/harp-protocol/harp/protocol/_payload.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/harp-protocol/harp/protocol/_payload.py b/src/harp-protocol/harp/protocol/_payload.py index 4130e30..f52dfcd 100644 --- a/src/harp-protocol/harp/protocol/_payload.py +++ b/src/harp-protocol/harp/protocol/_payload.py @@ -286,7 +286,8 @@ class PayloadBase(Generic[NpStructT]): def __init__(self, *args: object, **kwargs: object) -> None: cls = type(self) names = self.dtype.names - assert names is not None + if names is None: + raise TypeError(f"{type(self).__name__}.dtype has no named fields") # TODO this is just to allow; PayloadU16(foo) syntax. Not sure if it is worth it? if args and kwargs: From ca4ec5d9b589ab27311221f26af20fbe6dcb8433 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sat, 16 May 2026 21:47:29 -0700 Subject: [PATCH 214/267] Drop redundant code path --- src/harp-protocol/harp/protocol/_register.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/harp-protocol/harp/protocol/_register.py b/src/harp-protocol/harp/protocol/_register.py index bca7703..da2b6b9 100644 --- a/src/harp-protocol/harp/protocol/_register.py +++ b/src/harp-protocol/harp/protocol/_register.py @@ -196,8 +196,6 @@ def format( mt = MessageType.Write if message_type is None else message_type if isinstance(value, PayloadBase): raw = value.raw_payload.tobytes() - elif isinstance(value, np.ndarray) and value.dtype != cls.payload_type.numpy_dtype: - raw = value.tobytes() else: raw = np.asarray(value, dtype=cls.payload_type.numpy_dtype).tobytes() return build_message_frame( From 40c40008d14e87e0fb9daab795db59fe37fc8abc Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sat, 16 May 2026 21:48:38 -0700 Subject: [PATCH 215/267] Raise error if timestamp is explicitly requested --- src/harp-protocol/harp/protocol/_register.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/harp-protocol/harp/protocol/_register.py b/src/harp-protocol/harp/protocol/_register.py index da2b6b9..f49aefc 100644 --- a/src/harp-protocol/harp/protocol/_register.py +++ b/src/harp-protocol/harp/protocol/_register.py @@ -151,7 +151,9 @@ def read_dataframe( ) if timestamp: if timestamps is None: - timestamps = np.arange(len(payload), dtype=np.float64) + raise ValueError( + "Buffer contains no timestamp data; pass timestamp=False to suppress the timestamp column." + ) df.insert(0, "timestamp", timestamps) return df From de96c1762e75cf85e6743ba75b6e863e6adbf882 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sat, 16 May 2026 21:53:31 -0700 Subject: [PATCH 216/267] Cache bit-field definition --- src/harp-protocol/harp/protocol/_payload.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/harp-protocol/harp/protocol/_payload.py b/src/harp-protocol/harp/protocol/_payload.py index f52dfcd..9f5cb36 100644 --- a/src/harp-protocol/harp/protocol/_payload.py +++ b/src/harp-protocol/harp/protocol/_payload.py @@ -276,11 +276,19 @@ class PayloadBase(Generic[NpStructT]): buffers become Batch. """ + # Structured numpy dtype describing the memory layout of a single payload record. dtype: ClassVar[np.dtype] + # Field names shown in __repr__ and used as DataFrame column order. _repr_fields: ClassVar[tuple[str, ...]] + # The scalar twin of this class (identity for scalar classes, points to scalar from Batch). _scalar_cls: ClassVar["type[PayloadBase]"] + # The batch twin of this class (identity until the Batch sibling is generated). _batch_cls: ClassVar["type[PayloadBase]"] + # Cached map of attribute name → _BitFlag/_GroupMask descriptor, built once at class definition. + _bitfields: ClassVar[dict[str, Any]] + # Auto-generated sibling class whose descriptors return NDArray views instead of scalars. Batch: ClassVar["type[PayloadBase]"] + # The underlying numpy array holding one (0-D) or many (1-D) payload records. _arr: NDArray[NpStructT] def __init__(self, *args: object, **kwargs: object) -> None: @@ -302,7 +310,7 @@ def __init__(self, *args: object, **kwargs: object) -> None: slot_kwargs = {k: v for k, v in kwargs.items() if k in names} descriptor_kwargs = {k: v for k, v in kwargs.items() if k not in names} - bitfields = cls._collect_bitfields() + bitfields = cls._bitfields unknown = set(descriptor_kwargs) - set(bitfields) if unknown: raise TypeError(f"{cls.__name__}() got unexpected kwargs: {sorted(unknown)}") @@ -424,6 +432,8 @@ def __init_subclass__( _batch_of=cls, ) + cls._bitfields = cls._collect_bitfields() + @classmethod def from_array(cls, arr: "np.ndarray") -> Self: target = cls._scalar_cls if arr.ndim == 0 else cls._batch_cls @@ -509,6 +519,11 @@ def unwrap(cls, arr: "np.ndarray") -> Any: Struct payloads return a typed wrapper so descriptors like ``payload.Channel0`` work. Anonymous payloads override this to return the raw numpy scalar/ndarray directly. + + This allows us to not have to use hacky descriptors for single-field + struct payloads (e.g. a struct with one uint16 field can just be a + PayloadU16 subclass) while still supporting the full descriptor + machinery for multi-field struct payloads. """ return cls.from_array(arr) From ed9ddc510f67c06f9b6964c5bd2e230358bc9279 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sat, 16 May 2026 23:03:48 -0700 Subject: [PATCH 217/267] Create constants module and move tick value --- src/harp-protocol/harp/protocol/_builder.py | 5 ++-- src/harp-protocol/harp/protocol/_constants.py | 22 +++++++++++++++++ src/harp-protocol/harp/protocol/_message.py | 22 +++++++++++------ .../harp/protocol/_payload_type.py | 6 +++-- src/harp-protocol/harp/protocol/_register.py | 24 ++++++++++++------- 5 files changed, 60 insertions(+), 19 deletions(-) create mode 100644 src/harp-protocol/harp/protocol/_constants.py diff --git a/src/harp-protocol/harp/protocol/_builder.py b/src/harp-protocol/harp/protocol/_builder.py index e7b3d43..90e582f 100644 --- a/src/harp-protocol/harp/protocol/_builder.py +++ b/src/harp-protocol/harp/protocol/_builder.py @@ -2,6 +2,7 @@ import struct +from ._constants import _DEFAULT_PORT, _TICK_PERIOD_S from ._message_type import MessageType from ._message_type import message_type_to_byte as _msg_type_byte from ._payload_type import PayloadType, encode_payload_type @@ -13,13 +14,13 @@ def build_message_frame( payload_type: PayloadType, payload: bytes = b"", *, - port: int = 0xFF, + port: int = _DEFAULT_PORT, timestamp: float | None = None, ) -> bytes: """Build and return a complete Harp wire frame as bytes.""" if timestamp is not None: seconds = int(timestamp) - microseconds = round((timestamp - seconds) / 32e-6) + microseconds = round((timestamp - seconds) / _TICK_PERIOD_S) ts_bytes = struct.pack(" None: self._bytes: "bytes" = build_message_frame( @@ -62,7 +70,7 @@ def parse(cls, data: bytes | bytearray | memoryview) -> "HarpMessage": except ValueError as exc: raise HarpParseError(str(exc)) from exc - if bool(raw[4] & 0x10) and len(raw) < 5 + 6 + 1: + if bool(raw[4] & _TIMESTAMP_FLAG) and len(raw) < _HEADER_LEN + _TIMESTAMP_LEN + 1: raise HarpParseError("Frame too short to contain timestamp") obj = cls.__new__(cls) @@ -97,20 +105,20 @@ def payload_type(self) -> PayloadType: @property def has_timestamp(self) -> bool: """Return True if the timestamp flag is set in this message.""" - return bool(self._bytes[4] & 0x10) + return bool(self._bytes[4] & _TIMESTAMP_FLAG) @property def timestamp(self) -> float | None: """Return the timestamp of this message, or None if not present.""" if not self.has_timestamp: return None - seconds, microseconds = struct.unpack_from(" memoryview: """Payload bytes, excluding timestamp and checksum.""" - offset = 11 if self.has_timestamp else 5 + offset = _TIMESTAMPED_PAYLOAD_OFFSET if self.has_timestamp else _HEADER_LEN return memoryview(self._bytes)[offset:-1] @property @@ -137,7 +145,7 @@ def __init__( payload_type: PayloadType, payload: bytes = b"", *, - port: int = 0xFF, + port: int = _DEFAULT_PORT, timestamp: float | None = None, parsed: P, ) -> None: diff --git a/src/harp-protocol/harp/protocol/_payload_type.py b/src/harp-protocol/harp/protocol/_payload_type.py index c7f3264..7d37a3c 100644 --- a/src/harp-protocol/harp/protocol/_payload_type.py +++ b/src/harp-protocol/harp/protocol/_payload_type.py @@ -3,6 +3,8 @@ import numpy as np +from ._constants import _TIMESTAMP_FLAG + class PayloadType(Enum): """Harp scalar payload types. Each value is the corresponding numpy dtype.""" @@ -51,7 +53,7 @@ def decode_payload_type(b: int) -> PayloadTypeInfo: if b & 0x20: raise ValueError(f"Reserved bit 5 set in PayloadType byte: 0x{b:02x}") - has_timestamp = bool(b & 0x10) + has_timestamp = bool(b & _TIMESTAMP_FLAG) is_float = bool(b & 0x40) is_signed = bool(b & 0x80) @@ -84,5 +86,5 @@ def encode_payload_type(payload_type: PayloadType, *, has_timestamp: bool = Fals elif kind == "f": b |= 0x40 if has_timestamp: - b |= 0x10 + b |= _TIMESTAMP_FLAG return int(b) diff --git a/src/harp-protocol/harp/protocol/_register.py b/src/harp-protocol/harp/protocol/_register.py index f49aefc..ec3d96f 100644 --- a/src/harp-protocol/harp/protocol/_register.py +++ b/src/harp-protocol/harp/protocol/_register.py @@ -7,6 +7,14 @@ from typing_extensions import Sentinel from ._builder import build_message_frame +from ._constants import ( + _DEFAULT_PORT, + _HEADER_LEN, + _TICK_PERIOD_S, + _TIMESTAMP_FLAG, + _TIMESTAMPED_PAYLOAD_OFFSET, + _TS_MICROS_OFFSET, +) from ._message import HarpMessage from ._message_type import MessageType from ._payload import ( @@ -101,13 +109,13 @@ def parse_bulk( int(data[1]) + 2 ) # TODO this assumes all frames have the same length but we may want to revisit in the future. nrows = len(data) // stride - is_timestamped = bool(int(data[4]) & 0x10) - payload_offset = 11 if is_timestamped else 5 + is_timestamped = bool(int(data[4]) & _TIMESTAMP_FLAG) + payload_offset = _TIMESTAMPED_PAYLOAD_OFFSET if is_timestamped else _HEADER_LEN if is_timestamped and parse_timestamp: - ts_s = np.ndarray(nrows, dtype=" bytes: ... @overload @@ -175,7 +183,7 @@ def format( *, message_type: MessageType = MessageType.Write, timestamp: float | None = None, - port: int = 0xFF, + port: int = _DEFAULT_PORT, ) -> bytes: ... @final @@ -186,7 +194,7 @@ def format( *, message_type: MessageType | None = None, timestamp: float | None = None, - port: int = 0xFF, + port: int = _DEFAULT_PORT, ) -> bytes: """Build a Harp frame for this register. No value → Read; with value → Write.""" if value is _MISSING: From 8acfdf0c3026a6ef6928f8795e5d2c88ddf376a8 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sat, 16 May 2026 23:09:34 -0700 Subject: [PATCH 218/267] Refactor to use sentinel type --- src/harp-protocol/harp/protocol/_payload.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/harp-protocol/harp/protocol/_payload.py b/src/harp-protocol/harp/protocol/_payload.py index 9f5cb36..4444283 100644 --- a/src/harp-protocol/harp/protocol/_payload.py +++ b/src/harp-protocol/harp/protocol/_payload.py @@ -5,7 +5,7 @@ import numpy as np import pandas as pd from numpy.typing import NDArray -from typing_extensions import Self +from typing_extensions import Self, Sentinel from ._payload_converters import Converter as _Converter from ._payload_converters import IdentityConverter as _IdentityConverter @@ -213,7 +213,7 @@ def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: _PT = TypeVar("_PT", bound="PayloadBase[Any]") -_MISSING_INIT = object() +_MISSING_INIT = Sentinel("_MISSING_INIT") class Batch(Generic[_PT], ABC): From ee05e7bcd3cc9e37a2bfed5ef62e1aa0c8e4c833 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sat, 16 May 2026 23:13:54 -0700 Subject: [PATCH 219/267] Streamline checksum calculation --- src/harp-protocol/harp/protocol/_checksum.py | 8 ++------ src/harp-protocol/harp/protocol/_payload.py | 12 +++--------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/src/harp-protocol/harp/protocol/_checksum.py b/src/harp-protocol/harp/protocol/_checksum.py index c5720cf..34a45fc 100644 --- a/src/harp-protocol/harp/protocol/_checksum.py +++ b/src/harp-protocol/harp/protocol/_checksum.py @@ -1,15 +1,11 @@ def compute(data: bytes | bytearray | memoryview) -> int: """Wrapping u8 sum of all bytes except the last (the checksum byte itself).""" - total = 0 - mv = memoryview(bytes(data)) if not isinstance(data, (bytes, bytearray, memoryview)) else data - for b in mv[:-1]: - total = (total + b) & 0xFF - return total + return sum(memoryview(data)[:-1]) & 0xFF def validate(data: bytes | bytearray | memoryview) -> bool: """Return True if the last byte equals the checksum of all preceding bytes.""" - mv = memoryview(bytes(data)) if not isinstance(data, (bytes, bytearray, memoryview)) else data + mv = memoryview(data) if len(mv) < 2: return False return compute(mv) == mv[-1] diff --git a/src/harp-protocol/harp/protocol/_payload.py b/src/harp-protocol/harp/protocol/_payload.py index 4444283..8599813 100644 --- a/src/harp-protocol/harp/protocol/_payload.py +++ b/src/harp-protocol/harp/protocol/_payload.py @@ -1,6 +1,5 @@ import enum -from abc import ABC, abstractmethod -from typing import Any, ClassVar, Generic, TypeVar, final, overload +from typing import Any, ClassVar, Generic, Protocol, TypeVar, final, overload import numpy as np import pandas as pd @@ -205,7 +204,7 @@ def __init__( @overload def __get__(self, obj: None, owner: object = None) -> "_GroupMaskBatch[E]": ... @overload - def __get__(self, obj: "PayloadBase", owner: object = None) -> "NDArray[np.signedinteger]": ... + def __get__(self, obj: "PayloadBase", owner: object = None) -> "NDArray[Any]": ... def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: if obj is None: return self @@ -216,7 +215,7 @@ def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: _MISSING_INIT = Sentinel("_MISSING_INIT") -class Batch(Generic[_PT], ABC): +class Batch(Protocol[_PT]): """Alias type for batched payloads so we can have nice type-hinting for batch operations like `read_frames` and `to_dataframe`. @@ -233,13 +232,10 @@ class Batch(Generic[_PT], ABC): raw_payload: "NDArray[Any]" value: "NDArray[Any]" - @abstractmethod def __len__(self) -> int: ... # type: ignore[empty-body] - @abstractmethod def to_dataframe(self, *, decode_enums: bool = True) -> "pd.DataFrame": ... # type: ignore[empty-body] - @abstractmethod def __getattr__(self, name: str) -> "NDArray[Any]": ... # type: ignore[empty-body] @@ -547,8 +543,6 @@ class PayloadU16(AnonymousPayload, scalar_dtype=" Date: Sat, 16 May 2026 23:29:42 -0700 Subject: [PATCH 220/267] Be opinionated on format typing --- src/harp-protocol/harp/protocol/_register.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/harp-protocol/harp/protocol/_register.py b/src/harp-protocol/harp/protocol/_register.py index ec3d96f..c0496b2 100644 --- a/src/harp-protocol/harp/protocol/_register.py +++ b/src/harp-protocol/harp/protocol/_register.py @@ -114,7 +114,9 @@ def parse_bulk( if is_timestamped and parse_timestamp: ts_s = np.ndarray(nrows, dtype=" bytes: ... - + # We go with "U" for typing but it is worth noting that we accept "Any" below. + # However for API ergonomics we want to keep the type hint for symmetry @final @classmethod def format( @@ -206,6 +209,8 @@ def format( mt = MessageType.Write if message_type is None else message_type if isinstance(value, PayloadBase): raw = value.raw_payload.tobytes() + elif isinstance(value, np.ndarray): + raw = value.tobytes() else: raw = np.asarray(value, dtype=cls.payload_type.numpy_dtype).tobytes() return build_message_frame( From 9273314a99031f7f51c1ffbc22a1723732a90598 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sat, 16 May 2026 23:36:12 -0700 Subject: [PATCH 221/267] Update register syntax --- scripts/benchmark_analog_data.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/scripts/benchmark_analog_data.py b/scripts/benchmark_analog_data.py index 7171c8a..c3fc0c3 100644 --- a/scripts/benchmark_analog_data.py +++ b/scripts/benchmark_analog_data.py @@ -27,11 +27,14 @@ from typing import ClassVar import numpy as np + +sys.path.insert(0, str(Path(__file__).parent.parent)) from scripts._harp_io import read as harp_read # vendored harp-python read() -from harp.protocol._payload import PayloadBase, _Field, _IdentityConverter +from harp.protocol._payload import PayloadBase, _Field from harp.protocol._payload_type import PayloadType from harp.protocol._register import RegisterBase +from harp.protocol._payload_converters import Int16Converter REPO_ROOT = Path(__file__).parent.parent @@ -46,9 +49,9 @@ class AnalogDataPayload(PayloadBase[np.void]): """Payload for AnalogData (register 44): three signed 16-bit channels.""" - analog_input0 = _Field(_IdentityConverter(" Date: Sun, 17 May 2026 18:32:56 -0700 Subject: [PATCH 222/267] Use `dataclass_transform` for free type hint --- scripts/benchmark_analog_data.py | 6 +- scripts/core_registers_example.py | 2 +- scripts/heterogenous_register.py | 22 ++-- src/harp-device/src/harp/device/_registers.py | 80 ++++++------- src/harp-protocol/harp/protocol/__init__.py | 2 + src/harp-protocol/harp/protocol/_payload.py | 109 +++++++++++++++--- tests/protocol/test_converter.py | 35 +++--- tests/protocol/test_payload.py | 6 +- tests/protocol/test_register.py | 16 +-- tests/test_device.py | 22 ++-- 10 files changed, 187 insertions(+), 113 deletions(-) diff --git a/scripts/benchmark_analog_data.py b/scripts/benchmark_analog_data.py index c3fc0c3..f38b3b6 100644 --- a/scripts/benchmark_analog_data.py +++ b/scripts/benchmark_analog_data.py @@ -49,9 +49,9 @@ class AnalogDataPayload(PayloadBase[np.void]): """Payload for AnalogData (register 44): three signed 16-bit channels.""" - analog_input0 = _Field(Int16Converter()) - encoder = _Field(Int16Converter()) - analog_input1 = _Field(Int16Converter()) + analog_input0 = _Field(converter=Int16Converter()) + encoder = _Field(converter=Int16Converter()) + analog_input1 = _Field(converter=Int16Converter()) class AnalogData(RegisterBase[AnalogDataPayload]): diff --git a/scripts/core_registers_example.py b/scripts/core_registers_example.py index bd0074f..383f607 100644 --- a/scripts/core_registers_example.py +++ b/scripts/core_registers_example.py @@ -54,7 +54,7 @@ ), ), (ResetDevice, ResetDevicePayload(restore_default=True, restore_name=True)), - (DeviceName, DeviceNamePayload("my-harp-device")), + (DeviceName, DeviceNamePayload(value="my-harp-device")), (ClockConfig, ClockConfigPayload(clock_repeater=True, clock_unlock=True)), (Heartbeat, np.uint16(1)), (HwVersionH, np.uint8(2)), diff --git a/scripts/heterogenous_register.py b/scripts/heterogenous_register.py index 75ac8f0..7d96eb7 100644 --- a/scripts/heterogenous_register.py +++ b/scripts/heterogenous_register.py @@ -4,7 +4,7 @@ from harp.protocol._message import HarpMessage from harp.protocol._message_type import MessageType -from harp.protocol._payload import PayloadBase, _Field +from harp.protocol._payload import StructPayload, _Field from harp.protocol._payload_converters import StringConverter, UInt32Converter from harp.protocol._payload_type import PayloadType from harp.protocol._register import RegisterBase @@ -15,7 +15,7 @@ # FileSettings0: # address: 54 -# type: U8 +# payload_type: U8 # access: Write # length: 45 # description: "Struct to configure Analog Output Channel 0 @@ -23,17 +23,11 @@ # update_frequency_hz (U32), path (U8 array, 33 elements)" -class FileSettings0Payload(PayloadBase[np.uint8]): # np.uint8 is the word size - """_summary_ - - Args: - PayloadBase (_type_): _description_ - """ - - cycles = _Field(UInt32Converter()) - duration_us = _Field(UInt32Converter()) - update_frequency_hz = _Field(UInt32Converter()) - path = _Field(StringConverter(33)) +class FileSettings0Payload(StructPayload[np.uint8]): + cycles: np.uint32 = _Field(converter=UInt32Converter()) + duration_us: np.uint32 = _Field(converter=UInt32Converter()) + update_frequency_hz: np.uint32 = _Field(converter=UInt32Converter()) + path: str = _Field(converter=StringConverter(33)) # --------------------------------------------------------------------------- @@ -44,7 +38,7 @@ class FileSettings0Payload(PayloadBase[np.uint8]): # np.uint8 is the word size class FileSettings0(RegisterBase[FileSettings0Payload]): address: ClassVar[int] = 54 payload_type: ClassVar[PayloadType] = PayloadType.U8 - payload_class: ClassVar[type[PayloadBase]] = FileSettings0Payload + payload_class = FileSettings0Payload # --------------------------------------------------------------------------- diff --git a/src/harp-device/src/harp/device/_registers.py b/src/harp-device/src/harp/device/_registers.py index 99f52cf..f4debc4 100644 --- a/src/harp-device/src/harp/device/_registers.py +++ b/src/harp-device/src/harp/device/_registers.py @@ -2,13 +2,8 @@ from typing import ClassVar import numpy as np -from harp.protocol._payload import ( - PayloadBase, - _BitFlag, - _Field, - _GroupMask, - _StringConverter, -) +from harp.protocol._payload import StructPayload, _BitFlag, _Field, _GroupMask +from harp.protocol._payload_converters import StringConverter as _StringConverter from harp.protocol._payload_type import PayloadType from harp.protocol._register import RegisterBase, RegisterU8, RegisterU16, RegisterU32 @@ -33,48 +28,55 @@ class EnableFlag(enum.IntEnum): # --------------------------------------------------------------------------- -class OperationControlPayload(PayloadBase[np.uint8]): - operation_mode = _GroupMask(0x03, 0, OperationMode, dtype=np.uint8) - dump_registers = _BitFlag(0x08, dtype=np.uint8) - mute_replies = _BitFlag(0x10, dtype=np.uint8) - visual_indicators = _GroupMask(0x20, 5, EnableFlag, dtype=np.uint8) - operation_led = _GroupMask(0x40, 6, EnableFlag, dtype=np.uint8) - heartbeat = _GroupMask(0x80, 7, EnableFlag, dtype=np.uint8) - - -class ResetDevicePayload(PayloadBase[np.uint8]): +class OperationControlPayload(StructPayload[np.uint8]): + operation_mode: OperationMode = _GroupMask( + mask=0x03, shift=0, enum=OperationMode, dtype=np.uint8, default=OperationMode.Standby + ) + dump_registers: bool = _BitFlag(mask=0x08, dtype=np.uint8, default=False) + mute_replies: bool = _BitFlag(mask=0x10, dtype=np.uint8, default=False) + visual_indicators: EnableFlag = _GroupMask( + mask=0x20, shift=5, enum=EnableFlag, dtype=np.uint8, default=EnableFlag.Disabled + ) + operation_led: EnableFlag = _GroupMask( + mask=0x40, shift=6, enum=EnableFlag, dtype=np.uint8, default=EnableFlag.Disabled + ) + heartbeat: EnableFlag = _GroupMask( + mask=0x80, shift=7, enum=EnableFlag, dtype=np.uint8, default=EnableFlag.Disabled + ) + + +class ResetDevicePayload(StructPayload[np.uint8]): """Payload for the ResetDevice register (address 11).""" - restore_default = _BitFlag(0x01, dtype=np.uint8) - restore_eeprom = _BitFlag(0x02, dtype=np.uint8) - save = _BitFlag(0x04, dtype=np.uint8) - restore_name = _BitFlag(0x08, dtype=np.uint8) - update_firmware = _BitFlag(0x20, dtype=np.uint8) - boot_from_default = _BitFlag(0x40, dtype=np.uint8) - boot_from_eeprom = _BitFlag(0x80, dtype=np.uint8) + restore_default: bool = _BitFlag(mask=0x01, dtype=np.uint8, default=False) + restore_eeprom: bool = _BitFlag(mask=0x02, dtype=np.uint8, default=False) + save: bool = _BitFlag(mask=0x04, dtype=np.uint8, default=False) + restore_name: bool = _BitFlag(mask=0x08, dtype=np.uint8, default=False) + update_firmware: bool = _BitFlag(mask=0x20, dtype=np.uint8, default=False) + boot_from_default: bool = _BitFlag(mask=0x40, dtype=np.uint8, default=False) + boot_from_eeprom: bool = _BitFlag(mask=0x80, dtype=np.uint8, default=False) -class DeviceNamePayload(PayloadBase[np.uint8]): +class DeviceNamePayload(StructPayload[np.uint8]): """Payload for the DeviceName register (address 12). - Stores a user-specified ASCII device name padded to 25 bytes. Encoding, - decoding, and the dataframe column are all handled by ``_StringConverter``. + Stores a user-specified ASCII device name padded to 25 bytes. """ _MAX_LEN: ClassVar[int] = 25 - value = _Field(_StringConverter(_MAX_LEN)) + value: str = _Field(converter=_StringConverter(_MAX_LEN)) -class ClockConfigPayload(PayloadBase[np.uint8]): +class ClockConfigPayload(StructPayload[np.uint8]): """Payload for the ClockConfiguration register (address 14).""" - clock_repeater = _BitFlag(0x01, dtype=np.uint8) - clock_generator = _BitFlag(0x02, dtype=np.uint8) - repeater_capability = _BitFlag(0x08, dtype=np.uint8) - generator_capability = _BitFlag(0x10, dtype=np.uint8) - clock_unlock = _BitFlag(0x40, dtype=np.uint8) - clock_lock = _BitFlag(0x80, dtype=np.uint8) + clock_repeater: bool = _BitFlag(mask=0x01, dtype=np.uint8, default=False) + clock_generator: bool = _BitFlag(mask=0x02, dtype=np.uint8, default=False) + repeater_capability: bool = _BitFlag(mask=0x08, dtype=np.uint8, default=False) + generator_capability: bool = _BitFlag(mask=0x10, dtype=np.uint8, default=False) + clock_unlock: bool = _BitFlag(mask=0x40, dtype=np.uint8, default=False) + clock_lock: bool = _BitFlag(mask=0x80, dtype=np.uint8, default=False) # --------------------------------------------------------------------------- @@ -97,25 +99,25 @@ class TimestampMicro(RegisterU16): class OperationControl(RegisterBase[OperationControlPayload]): address: ClassVar[int] = 10 payload_type: ClassVar[PayloadType] = PayloadType.U8 - payload_class: ClassVar[type[PayloadBase]] = OperationControlPayload + payload_class = OperationControlPayload class ResetDevice(RegisterBase[ResetDevicePayload]): address: ClassVar[int] = 11 payload_type: ClassVar[PayloadType] = PayloadType.U8 - payload_class: ClassVar[type[PayloadBase]] = ResetDevicePayload + payload_class = ResetDevicePayload class DeviceName(RegisterBase[DeviceNamePayload]): address: ClassVar[int] = 12 payload_type: ClassVar[PayloadType] = PayloadType.U8 - payload_class: ClassVar[type[PayloadBase]] = DeviceNamePayload + payload_class = DeviceNamePayload class ClockConfig(RegisterBase[ClockConfigPayload]): address: ClassVar[int] = 14 payload_type: ClassVar[PayloadType] = PayloadType.U8 - payload_class: ClassVar[type[PayloadBase]] = ClockConfigPayload + payload_class = ClockConfigPayload class Heartbeat(RegisterU16): diff --git a/src/harp-protocol/harp/protocol/__init__.py b/src/harp-protocol/harp/protocol/__init__.py index ef21618..20dee50 100644 --- a/src/harp-protocol/harp/protocol/__init__.py +++ b/src/harp-protocol/harp/protocol/__init__.py @@ -15,6 +15,7 @@ ) from ._payload import ( PayloadBase, + StructPayload, PayloadU8, PayloadU16, PayloadU32, @@ -82,6 +83,7 @@ "converter_registry", # Payload DSL "PayloadBase", + "StructPayload", "PayloadU8", "PayloadU16", "PayloadU32", diff --git a/src/harp-protocol/harp/protocol/_payload.py b/src/harp-protocol/harp/protocol/_payload.py index 8599813..018c7b0 100644 --- a/src/harp-protocol/harp/protocol/_payload.py +++ b/src/harp-protocol/harp/protocol/_payload.py @@ -1,10 +1,10 @@ import enum -from typing import Any, ClassVar, Generic, Protocol, TypeVar, final, overload +from typing import TYPE_CHECKING, Any, ClassVar, Generic, Protocol, TypeVar, final, overload import numpy as np import pandas as pd from numpy.typing import NDArray -from typing_extensions import Self, Sentinel +from typing_extensions import Self, Sentinel, dataclass_transform from ._payload_converters import Converter as _Converter from ._payload_converters import IdentityConverter as _IdentityConverter @@ -13,6 +13,8 @@ T = TypeVar("T") E = TypeVar("E", bound=enum.IntEnum) +_MISSING = Sentinel("_MISSING") + # --------------------------------------------------------------------------- # Descriptors — scalar variants (return Python / 0-D types) @@ -24,13 +26,19 @@ class _Field(Generic[T]): Note: The name of the field is used as the slot name in the underlying numpy structured array. """ - def __init__(self, converter: _Converter[T], *, name: str | None = None) -> None: + if TYPE_CHECKING: + # Makes `field: T = _Field(converter=...)` valid under @dataclass_transform without a + # type-mismatch error. At runtime __new__ is not defined and a _Field instance is + # returned as normal. + def __new__(cls, *, converter: "_Converter[T]", default: "T" = ...) -> "T": ... # type: ignore[misc] + + def __init__(self, *, converter: _Converter[T], default: object = _MISSING) -> None: self._converter = converter - self._name = name + self._name: str | None = None + self._default = default def __set_name__(self, owner: object, name: str) -> None: - if self._name is None: - self._name = name + self._name = name @overload def __get__(self, obj: None, owner: object = None) -> "_Field[T]": ... @@ -42,7 +50,7 @@ def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: return self._converter.decode_scalar(obj._arr[self._name]) def _to_batch(self) -> "_FieldBatch[T]": - return _FieldBatch(self._converter, name=self._name) + return _FieldBatch(converter=self._converter) class _BitFlag: @@ -52,16 +60,29 @@ class _BitFlag: 2) An additional "slot" name can be provided if the bit flag is not stored in the same-named slot as the field descriptors (e.g. if multiple bit flags are packed into a single byte slot). """ + if TYPE_CHECKING: + + def __new__( + cls, + *, + mask: int, + slot: str = "value", + dtype: "np.dtype | str | type" = np.uint8, + default: bool = ..., + ) -> bool: ... # type: ignore[misc] + def __init__( self, - mask: int, *, + mask: int, slot: str = "value", dtype: "np.dtype | str | type" = np.uint8, + default: object = _MISSING, ) -> None: self._mask = mask self._slot = slot self._dtype = np.dtype(dtype) + self._default = default @overload def __get__(self, obj: None, owner: object = None) -> "_BitFlag": ... @@ -95,20 +116,35 @@ class _GroupMask(Generic[E]): 2) An additional "slot" name can be provided if the group mask is not stored in the same-named slot as the field descriptors (e.g. if multiple group masks are packed into a single byte slot). """ + if TYPE_CHECKING: + + def __new__( + cls, + *, + mask: int, + shift: int, + enum: "type[E]", + slot: str = "value", + dtype: "np.dtype | str | type" = np.uint8, + default: "E" = ..., + ) -> "E": ... # type: ignore[misc] + def __init__( self, + *, mask: int, shift: int, enum: type[E], - *, slot: str = "value", dtype: "np.dtype | str | type" = np.uint8, + default: object = _MISSING, ) -> None: self._mask = mask self._shift = shift self._enum = enum self._slot = slot self._dtype = np.dtype(dtype) + self._default = default self._categories, self._code_lookup = _build_enum_lookup(enum) @overload @@ -140,13 +176,12 @@ def _to_batch(self) -> "_GroupMaskBatch[E]": class _FieldBatch(Generic[T]): """Same as _Field but returns an NDArray view for batch payloads rather than a scalar value.""" - def __init__(self, converter: _Converter[T], *, name: str | None = None) -> None: + def __init__(self, *, converter: _Converter[T]) -> None: self._converter = converter - self._name = name + self._name: str | None = None def __set_name__(self, owner: object, name: str) -> None: - if self._name is None: - self._name = name + self._name = name @overload def __get__(self, obj: None, owner: object = None) -> "_FieldBatch[T]": ... @@ -282,6 +317,8 @@ class PayloadBase(Generic[NpStructT]): _batch_cls: ClassVar["type[PayloadBase]"] # Cached map of attribute name → _BitFlag/_GroupMask descriptor, built once at class definition. _bitfields: ClassVar[dict[str, Any]] + # Cached map of attribute name → default value for fields that declare one. + _defaults: ClassVar[dict[str, Any]] # Auto-generated sibling class whose descriptors return NDArray views instead of scalars. Batch: ClassVar["type[PayloadBase]"] # The underlying numpy array holding one (0-D) or many (1-D) payload records. @@ -303,6 +340,13 @@ def __init__(self, *args: object, **kwargs: object) -> None: raise TypeError(f"{cls.__name__}() takes exactly one positional argument") kwargs = {names[0]: args[0]} + defaults = cls._defaults + if defaults: + merged = {k: v for k, v in defaults.items() if k not in kwargs} + if merged: + merged.update(kwargs) + kwargs = merged + slot_kwargs = {k: v for k, v in kwargs.items() if k in names} descriptor_kwargs = {k: v for k, v in kwargs.items() if k not in names} @@ -353,6 +397,15 @@ def _collect_bitfields( out[attr] = val return out + @classmethod + def _collect_defaults(cls) -> "dict[str, Any]": + out: dict[str, Any] = {} + for klass in reversed(cls.__mro__): + for attr, val in klass.__dict__.items(): + if isinstance(val, (_Field, _BitFlag, _GroupMask)) and val._default is not _MISSING: + out[attr] = val._default + return out + def __init_subclass__( cls, *, @@ -385,8 +438,6 @@ def __init_subclass__( slots: dict[str, np.dtype] = {} for attr_name, val in own_declarations: if isinstance(val, _Field): - if val._name is None: - val._name = attr_name slot, dtype = val._name, val._converter.dtype else: slot, dtype = val._slot, val._dtype @@ -429,6 +480,7 @@ def __init_subclass__( ) cls._bitfields = cls._collect_bitfields() + cls._defaults = cls._collect_defaults() @classmethod def from_array(cls, arr: "np.ndarray") -> Self: @@ -440,7 +492,7 @@ def from_array(cls, arr: "np.ndarray") -> Self: @classmethod def from_buffer(cls, buf: bytes | bytearray | memoryview) -> Self: arr = np.frombuffer(buf, dtype=cls.dtype) - return cls.from_array(arr) + return cls.from_array(arr[0] if len(arr) == 1 else arr) @property def raw_payload(self) -> NDArray[NpStructT]: @@ -524,6 +576,31 @@ def unwrap(cls, arr: "np.ndarray") -> Any: return cls.from_array(arr) +# --------------------------------------------------------------------------- +# StructPayload — base for named-field (struct) register payloads +# --------------------------------------------------------------------------- + + +@dataclass_transform( + kw_only_default=True, + field_specifiers=(_Field, _BitFlag, _GroupMask), +) +class StructPayload(PayloadBase[NpStructT]): + """Base class for struct register payloads with typed field descriptors. + + Subclasses declare fields using ``_Field``, ``_BitFlag``, or ``_GroupMask`` + descriptors. Type checkers synthesize a keyword-only ``__init__`` from + those declarations, so constructor calls are fully type-checked and have + IDE autocompletion. + + Example:: + + class MyPayload(StructPayload[np.uint8]): + channel = _Field(UInt16Converter()) + enabled = _BitFlag(0x01, dtype=np.uint8) + """ + + # --------------------------------------------------------------------------- # AnonymousPayload — single unnamed slot, no user-facing descriptors # --------------------------------------------------------------------------- diff --git a/tests/protocol/test_converter.py b/tests/protocol/test_converter.py index 4464217..19571f7 100644 --- a/tests/protocol/test_converter.py +++ b/tests/protocol/test_converter.py @@ -18,8 +18,8 @@ _Field, _GroupMask, _IdentityConverter, - _StringConverter, ) +from harp.protocol._payload_converters import StringConverter as _StringConverter # --------------------------------------------------------------------------- @@ -39,8 +39,8 @@ class _Color(enum.IntEnum): class _NumericPayload(PayloadBase): - a = _Field(_IdentityConverter(" pd.DataFrame: return pd.DataFrame( diff --git a/tests/protocol/test_register.py b/tests/protocol/test_register.py index ca7ca94..118f4bf 100644 --- a/tests/protocol/test_register.py +++ b/tests/protocol/test_register.py @@ -49,9 +49,9 @@ class DigitalOutputSet(RegisterU16): class AnalogDataPayload(PayloadBase): - analog_input0 = _Field(_IdentityConverter(" Date: Sun, 17 May 2026 18:33:07 -0700 Subject: [PATCH 223/267] Test defaults --- scripts/heterogenous_register.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/scripts/heterogenous_register.py b/scripts/heterogenous_register.py index 7d96eb7..0b4e919 100644 --- a/scripts/heterogenous_register.py +++ b/scripts/heterogenous_register.py @@ -15,7 +15,7 @@ # FileSettings0: # address: 54 -# payload_type: U8 +# type: U8 # access: Write # length: 45 # description: "Struct to configure Analog Output Channel 0 @@ -27,7 +27,7 @@ class FileSettings0Payload(StructPayload[np.uint8]): cycles: np.uint32 = _Field(converter=UInt32Converter()) duration_us: np.uint32 = _Field(converter=UInt32Converter()) update_frequency_hz: np.uint32 = _Field(converter=UInt32Converter()) - path: str = _Field(converter=StringConverter(33)) + path: str = _Field(converter=StringConverter(33), default="220khzwaveform") # defaults work too # --------------------------------------------------------------------------- @@ -37,7 +37,7 @@ class FileSettings0Payload(StructPayload[np.uint8]): class FileSettings0(RegisterBase[FileSettings0Payload]): address: ClassVar[int] = 54 - payload_type: ClassVar[PayloadType] = PayloadType.U8 + payload_type = PayloadType.U8 payload_class = FileSettings0Payload @@ -48,10 +48,11 @@ class FileSettings0(RegisterBase[FileSettings0Payload]): if __name__ == "__main__": # 1. Build payload payload = FileSettings0Payload( # The stubs for the constructor must be auto generated, but this already works - cycles=3, - duration_us=250_000, + cycles=np.uint32( + 3 + ), # If you want to be correct, you should use the exact types for the fields, but the converters will handle it if you don't + duration_us=250_000, # however this still works update_frequency_hz=400, - path="220khzwaveform", ) print(f"Payload dtype : {FileSettings0Payload.dtype}") print(f"Payload bytes : {payload.raw_payload.tobytes().hex()}") From 3bcab95054824609c369d3a240ec12aa83f6136c Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sun, 17 May 2026 20:50:00 -0700 Subject: [PATCH 224/267] Cleanup public api members --- scripts/benchmark_analog_data.py | 11 ++-- scripts/core_registers_example.py | 4 +- scripts/heterogenous_register.py | 24 +++++---- src/harp-device/src/harp/device/__init__.py | 12 ++++- src/harp-device/src/harp/device/_registers.py | 42 +++++++-------- src/harp-protocol/harp/protocol/__init__.py | 28 +++------- src/harp-protocol/harp/protocol/_payload.py | 52 ++++++++++--------- tests/protocol/test_converter.py | 34 ++++++------ tests/protocol/test_payload.py | 8 +-- tests/protocol/test_register.py | 18 +++---- tests/test_device.py | 26 +++++----- 11 files changed, 130 insertions(+), 129 deletions(-) diff --git a/scripts/benchmark_analog_data.py b/scripts/benchmark_analog_data.py index f38b3b6..5006e10 100644 --- a/scripts/benchmark_analog_data.py +++ b/scripts/benchmark_analog_data.py @@ -31,10 +31,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent)) from scripts._harp_io import read as harp_read # vendored harp-python read() -from harp.protocol._payload import PayloadBase, _Field -from harp.protocol._payload_type import PayloadType -from harp.protocol._register import RegisterBase -from harp.protocol._payload_converters import Int16Converter +from harp.protocol import PayloadBase, Field, PayloadType, RegisterBase, IdentityConverter REPO_ROOT = Path(__file__).parent.parent @@ -49,9 +46,9 @@ class AnalogDataPayload(PayloadBase[np.void]): """Payload for AnalogData (register 44): three signed 16-bit channels.""" - analog_input0 = _Field(converter=Int16Converter()) - encoder = _Field(converter=Int16Converter()) - analog_input1 = _Field(converter=Int16Converter()) + analog_input0 = Field(converter=IdentityConverter(np.int16)) + encoder = Field(converter=IdentityConverter(np.int16)) + analog_input1 = Field(converter=IdentityConverter(np.int16)) class AnalogData(RegisterBase[AnalogDataPayload]): diff --git a/scripts/core_registers_example.py b/scripts/core_registers_example.py index 383f607..b02c4ad 100644 --- a/scripts/core_registers_example.py +++ b/scripts/core_registers_example.py @@ -8,7 +8,7 @@ import numpy as np -from harp.device._registers import ( +from harp.device import ( # Enums / flags EnableFlag, OperationMode, @@ -37,7 +37,7 @@ WhoAmI, ) -from harp.protocol._message import HarpMessage +from harp.protocol import HarpMessage examples = [ (WhoAmI, np.uint16(1216)), diff --git a/scripts/heterogenous_register.py b/scripts/heterogenous_register.py index 0b4e919..b1e85fc 100644 --- a/scripts/heterogenous_register.py +++ b/scripts/heterogenous_register.py @@ -2,12 +2,16 @@ import numpy as np -from harp.protocol._message import HarpMessage -from harp.protocol._message_type import MessageType -from harp.protocol._payload import StructPayload, _Field -from harp.protocol._payload_converters import StringConverter, UInt32Converter -from harp.protocol._payload_type import PayloadType -from harp.protocol._register import RegisterBase +from harp.protocol import ( + HarpMessage, + MessageType, + StructPayload, + Field, + StringConverter, + IdentityConverter, + PayloadType, + RegisterBase, +) # --------------------------------------------------------------------------- # Payload @@ -24,10 +28,10 @@ class FileSettings0Payload(StructPayload[np.uint8]): - cycles: np.uint32 = _Field(converter=UInt32Converter()) - duration_us: np.uint32 = _Field(converter=UInt32Converter()) - update_frequency_hz: np.uint32 = _Field(converter=UInt32Converter()) - path: str = _Field(converter=StringConverter(33), default="220khzwaveform") # defaults work too + cycles: np.uint32 = Field(converter=IdentityConverter(np.uint32)) + duration_us: np.uint32 = Field(converter=IdentityConverter(np.uint32)) + update_frequency_hz: np.uint32 = Field(converter=IdentityConverter(np.uint32)) + path: str = Field(converter=StringConverter(33), default="220khzwaveform") # defaults work too # --------------------------------------------------------------------------- diff --git a/src/harp-device/src/harp/device/__init__.py b/src/harp-device/src/harp/device/__init__.py index 6668b5b..d7fc0b8 100644 --- a/src/harp-device/src/harp/device/__init__.py +++ b/src/harp-device/src/harp/device/__init__.py @@ -3,8 +3,12 @@ from ._registers import ( AssemblyVersion, ClockConfig, + ClockConfigPayload, CoreVersionH, CoreVersionL, + DeviceName, + DeviceNamePayload, + EnableFlag, FirmwareVersionH, FirmwareVersionL, Heartbeat, @@ -14,6 +18,7 @@ OperationControlPayload, OperationMode, ResetDevice, + ResetDevicePayload, SerialNumber, TimestampMicro, TimestampOffset, @@ -40,8 +45,13 @@ "OperationControlPayload", "OperationMode", "ResetDevice", - "SerialNumber", + "ResetDevicePayload", + "DeviceName", + "DeviceNamePayload", + "EnableFlag", "ClockConfig", + "ClockConfigPayload", + "SerialNumber", "TimestampOffset", "Heartbeat", ] diff --git a/src/harp-device/src/harp/device/_registers.py b/src/harp-device/src/harp/device/_registers.py index f4debc4..81d0192 100644 --- a/src/harp-device/src/harp/device/_registers.py +++ b/src/harp-device/src/harp/device/_registers.py @@ -2,7 +2,7 @@ from typing import ClassVar import numpy as np -from harp.protocol._payload import StructPayload, _BitFlag, _Field, _GroupMask +from harp.protocol._payload import StructPayload, BitFlag, Field, GroupMask from harp.protocol._payload_converters import StringConverter as _StringConverter from harp.protocol._payload_type import PayloadType from harp.protocol._register import RegisterBase, RegisterU8, RegisterU16, RegisterU32 @@ -29,18 +29,18 @@ class EnableFlag(enum.IntEnum): class OperationControlPayload(StructPayload[np.uint8]): - operation_mode: OperationMode = _GroupMask( + operation_mode: OperationMode = GroupMask( mask=0x03, shift=0, enum=OperationMode, dtype=np.uint8, default=OperationMode.Standby ) - dump_registers: bool = _BitFlag(mask=0x08, dtype=np.uint8, default=False) - mute_replies: bool = _BitFlag(mask=0x10, dtype=np.uint8, default=False) - visual_indicators: EnableFlag = _GroupMask( + dump_registers: bool = BitFlag(mask=0x08, dtype=np.uint8, default=False) + mute_replies: bool = BitFlag(mask=0x10, dtype=np.uint8, default=False) + visual_indicators: EnableFlag = GroupMask( mask=0x20, shift=5, enum=EnableFlag, dtype=np.uint8, default=EnableFlag.Disabled ) - operation_led: EnableFlag = _GroupMask( + operation_led: EnableFlag = GroupMask( mask=0x40, shift=6, enum=EnableFlag, dtype=np.uint8, default=EnableFlag.Disabled ) - heartbeat: EnableFlag = _GroupMask( + heartbeat: EnableFlag = GroupMask( mask=0x80, shift=7, enum=EnableFlag, dtype=np.uint8, default=EnableFlag.Disabled ) @@ -48,13 +48,13 @@ class OperationControlPayload(StructPayload[np.uint8]): class ResetDevicePayload(StructPayload[np.uint8]): """Payload for the ResetDevice register (address 11).""" - restore_default: bool = _BitFlag(mask=0x01, dtype=np.uint8, default=False) - restore_eeprom: bool = _BitFlag(mask=0x02, dtype=np.uint8, default=False) - save: bool = _BitFlag(mask=0x04, dtype=np.uint8, default=False) - restore_name: bool = _BitFlag(mask=0x08, dtype=np.uint8, default=False) - update_firmware: bool = _BitFlag(mask=0x20, dtype=np.uint8, default=False) - boot_from_default: bool = _BitFlag(mask=0x40, dtype=np.uint8, default=False) - boot_from_eeprom: bool = _BitFlag(mask=0x80, dtype=np.uint8, default=False) + restore_default: bool = BitFlag(mask=0x01, dtype=np.uint8, default=False) + restore_eeprom: bool = BitFlag(mask=0x02, dtype=np.uint8, default=False) + save: bool = BitFlag(mask=0x04, dtype=np.uint8, default=False) + restore_name: bool = BitFlag(mask=0x08, dtype=np.uint8, default=False) + update_firmware: bool = BitFlag(mask=0x20, dtype=np.uint8, default=False) + boot_from_default: bool = BitFlag(mask=0x40, dtype=np.uint8, default=False) + boot_from_eeprom: bool = BitFlag(mask=0x80, dtype=np.uint8, default=False) class DeviceNamePayload(StructPayload[np.uint8]): @@ -65,18 +65,18 @@ class DeviceNamePayload(StructPayload[np.uint8]): _MAX_LEN: ClassVar[int] = 25 - value: str = _Field(converter=_StringConverter(_MAX_LEN)) + value: str = Field(converter=_StringConverter(_MAX_LEN)) class ClockConfigPayload(StructPayload[np.uint8]): """Payload for the ClockConfiguration register (address 14).""" - clock_repeater: bool = _BitFlag(mask=0x01, dtype=np.uint8, default=False) - clock_generator: bool = _BitFlag(mask=0x02, dtype=np.uint8, default=False) - repeater_capability: bool = _BitFlag(mask=0x08, dtype=np.uint8, default=False) - generator_capability: bool = _BitFlag(mask=0x10, dtype=np.uint8, default=False) - clock_unlock: bool = _BitFlag(mask=0x40, dtype=np.uint8, default=False) - clock_lock: bool = _BitFlag(mask=0x80, dtype=np.uint8, default=False) + clock_repeater: bool = BitFlag(mask=0x01, dtype=np.uint8, default=False) + clock_generator: bool = BitFlag(mask=0x02, dtype=np.uint8, default=False) + repeater_capability: bool = BitFlag(mask=0x08, dtype=np.uint8, default=False) + generator_capability: bool = BitFlag(mask=0x10, dtype=np.uint8, default=False) + clock_unlock: bool = BitFlag(mask=0x40, dtype=np.uint8, default=False) + clock_lock: bool = BitFlag(mask=0x80, dtype=np.uint8, default=False) # --------------------------------------------------------------------------- diff --git a/src/harp-protocol/harp/protocol/__init__.py b/src/harp-protocol/harp/protocol/__init__.py index 20dee50..851946b 100644 --- a/src/harp-protocol/harp/protocol/__init__.py +++ b/src/harp-protocol/harp/protocol/__init__.py @@ -1,21 +1,17 @@ -from ._builder import build_message_frame -from ._checksum import compute as compute_checksum, validate as validate_checksum from ._message import HarpMessage, HarpParseError, ParsedHarpMessage -from ._message_type import ( - MessageType, - message_type_from_byte as message_type_from_byte, - message_type_to_byte as message_type_to_byte, -) +from ._message_type import MessageType from ._payload_converters import ( Converter, IdentityConverter, StringConverter, - converter_registry, register_converter, ) from ._payload import ( PayloadBase, StructPayload, + Field, + BitFlag, + GroupMask, PayloadU8, PayloadU16, PayloadU32, @@ -35,7 +31,7 @@ PayloadS64Array, PayloadFloatArray, ) -from ._payload_type import PayloadType, PayloadTypeInfo, decode_payload_type, encode_payload_type +from ._payload_type import PayloadType from ._register import ( RegisterBase, RegisterU8, @@ -61,16 +57,8 @@ __all__ = [ # Message type "MessageType", - "message_type_from_byte", - "message_type_to_byte", # Payload type "PayloadType", - "PayloadTypeInfo", - "decode_payload_type", - "encode_payload_type", - # Checksum - "compute_checksum", - "validate_checksum", # Message "HarpMessage", "ParsedHarpMessage", @@ -80,10 +68,12 @@ "IdentityConverter", "StringConverter", "register_converter", - "converter_registry", # Payload DSL "PayloadBase", "StructPayload", + "Field", + "BitFlag", + "GroupMask", "PayloadU8", "PayloadU16", "PayloadU32", @@ -124,6 +114,4 @@ "RegisterS32Array", "RegisterS64Array", "RegisterFloatArray", - # Frame builders - "build_message_frame", ] diff --git a/src/harp-protocol/harp/protocol/_payload.py b/src/harp-protocol/harp/protocol/_payload.py index 018c7b0..cdcbfb8 100644 --- a/src/harp-protocol/harp/protocol/_payload.py +++ b/src/harp-protocol/harp/protocol/_payload.py @@ -21,14 +21,14 @@ # --------------------------------------------------------------------------- -class _Field(Generic[T]): +class Field(Generic[T]): """Descriptor for a payload field with a Converter. Note: The name of the field is used as the slot name in the underlying numpy structured array. """ if TYPE_CHECKING: - # Makes `field: T = _Field(converter=...)` valid under @dataclass_transform without a - # type-mismatch error. At runtime __new__ is not defined and a _Field instance is + # Makes `field: T = Field(converter=...)` valid under @dataclass_transform without a + # type-mismatch error. At runtime __new__ is not defined and a Field instance is # returned as normal. def __new__(cls, *, converter: "_Converter[T]", default: "T" = ...) -> "T": ... # type: ignore[misc] @@ -41,7 +41,7 @@ def __set_name__(self, owner: object, name: str) -> None: self._name = name @overload - def __get__(self, obj: None, owner: object = None) -> "_Field[T]": ... + def __get__(self, obj: None, owner: object = None) -> "Field[T]": ... @overload def __get__(self, obj: "PayloadBase", owner: object = None) -> T: ... def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: @@ -53,7 +53,7 @@ def _to_batch(self) -> "_FieldBatch[T]": return _FieldBatch(converter=self._converter) -class _BitFlag: +class BitFlag: """Descriptor for a bit flag within a payload field. Notes: 1) ensure dtype of the slot can hold the mask (e.g. uint8 for mask 0x01, uint16 for mask 0x100, etc.) @@ -69,7 +69,7 @@ def __new__( slot: str = "value", dtype: "np.dtype | str | type" = np.uint8, default: bool = ..., - ) -> bool: ... # type: ignore[misc] + ) -> bool: ... # type: ignore[misc] # noqa: E704 def __init__( self, @@ -81,11 +81,13 @@ def __init__( ) -> None: self._mask = mask self._slot = slot - self._dtype = np.dtype(dtype) + self._dtype = np.dtype( + dtype + ) # TODO I am not sure we need this. In theory we could just assume it uses the word size of the Payload dtype and validate that the mask fits within it, rather than storing a separate dtype here. self._default = default @overload - def __get__(self, obj: None, owner: object = None) -> "_BitFlag": ... + def __get__(self, obj: None, owner: object = None) -> "BitFlag": ... @overload def __get__(self, obj: "PayloadBase", owner: object = None) -> bool: ... def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: @@ -98,7 +100,7 @@ def _to_batch(self) -> "_BitFlagBatch": def _build_enum_lookup(enum_cls: type) -> "tuple[list[str], np.ndarray]": - """Helper for _GroupMask to build the category list and code lookup table for a given enum.IntEnum class.""" + """Helper for GroupMask to build the category list and code lookup table for a given enum.IntEnum class.""" members = list(enum_cls) categories = [m.name for m in members] max_val = max(int(m) for m in members) @@ -109,7 +111,7 @@ def _build_enum_lookup(enum_cls: type) -> "tuple[list[str], np.ndarray]": return categories, code_lookup -class _GroupMask(Generic[E]): +class GroupMask(Generic[E]): """Descriptor for a group of bits representing an enum value within a payload field. Notes: 1) ensure dtype of the slot can hold the mask (e.g. uint8 for mask 0x01, uint16 for mask 0x100, etc.) @@ -127,7 +129,7 @@ def __new__( slot: str = "value", dtype: "np.dtype | str | type" = np.uint8, default: "E" = ..., - ) -> "E": ... # type: ignore[misc] + ) -> "E": ... # type: ignore[misc] # noqa: E704 def __init__( self, @@ -148,7 +150,7 @@ def __init__( self._categories, self._code_lookup = _build_enum_lookup(enum) @overload - def __get__(self, obj: None, owner: object = None) -> "_GroupMask[E]": ... + def __get__(self, obj: None, owner: object = None) -> "GroupMask[E]": ... @overload def __get__(self, obj: "PayloadBase", owner: object = None) -> E: ... def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: @@ -275,13 +277,13 @@ def __getattr__(self, name: str) -> "NDArray[Any]": ... # type: ignore[empty-bo # Helpers for type checking using isinstance() -_SCALAR_DECLARATION_TYPES = (_Field, _BitFlag, _GroupMask) +_SCALAR_DECLARATION_TYPES = (Field, BitFlag, GroupMask) _BATCH_DECLARATION_TYPES = (_FieldBatch, _BitFlagBatch, _GroupMaskBatch) _DECLARATION_TYPES = _SCALAR_DECLARATION_TYPES + _BATCH_DECLARATION_TYPES -_BITFIELD_TYPES = (_BitFlag, _GroupMask, _BitFlagBatch, _GroupMaskBatch) -_FIELD_TYPES = (_Field, _FieldBatch) -_GROUP_MASK_TYPES = (_GroupMask, _GroupMaskBatch) -_BIT_FLAG_TYPES = (_BitFlag, _BitFlagBatch) +_BITFIELD_TYPES = (BitFlag, GroupMask, _BitFlagBatch, _GroupMaskBatch) +_FIELD_TYPES = (Field, _FieldBatch) +_GROUP_MASK_TYPES = (GroupMask, _GroupMaskBatch) +_BIT_FLAG_TYPES = (BitFlag, _BitFlagBatch) # value/raw_payload deliberately omitted: overriding them is the intended # pattern for single-slot converter-driven payloads. @@ -389,7 +391,7 @@ def _mro_descriptor(cls, name: str) -> object | None: @classmethod def _collect_bitfields( cls, - ) -> "dict[str, _BitFlag | _GroupMask | _BitFlagBatch | _GroupMaskBatch]": + ) -> "dict[str, BitFlag | GroupMask | _BitFlagBatch | _GroupMaskBatch]": out: dict[str, Any] = {} for klass in reversed(cls.__mro__): for attr, val in klass.__dict__.items(): @@ -402,7 +404,7 @@ def _collect_defaults(cls) -> "dict[str, Any]": out: dict[str, Any] = {} for klass in reversed(cls.__mro__): for attr, val in klass.__dict__.items(): - if isinstance(val, (_Field, _BitFlag, _GroupMask)) and val._default is not _MISSING: + if isinstance(val, (Field, BitFlag, GroupMask)) and val._default is not _MISSING: out[attr] = val._default return out @@ -437,7 +439,7 @@ def __init_subclass__( if own_declarations: slots: dict[str, np.dtype] = {} for attr_name, val in own_declarations: - if isinstance(val, _Field): + if isinstance(val, Field): slot, dtype = val._name, val._converter.dtype else: slot, dtype = val._slot, val._dtype @@ -453,7 +455,7 @@ def __init_subclass__( if "_repr_fields" not in cls.__dict__: bitfield_names = tuple( - name for name, val in vars(cls).items() if isinstance(val, (_BitFlag, _GroupMask)) + name for name, val in vars(cls).items() if isinstance(val, (BitFlag, GroupMask)) ) if bitfield_names: cls._repr_fields = bitfield_names @@ -583,12 +585,12 @@ def unwrap(cls, arr: "np.ndarray") -> Any: @dataclass_transform( kw_only_default=True, - field_specifiers=(_Field, _BitFlag, _GroupMask), + field_specifiers=(Field, BitFlag, GroupMask), ) class StructPayload(PayloadBase[NpStructT]): """Base class for struct register payloads with typed field descriptors. - Subclasses declare fields using ``_Field``, ``_BitFlag``, or ``_GroupMask`` + Subclasses declare fields using ``Field``, ``BitFlag``, or ``GroupMask`` descriptors. Type checkers synthesize a keyword-only ``__init__`` from those declarations, so constructor calls are fully type-checked and have IDE autocompletion. @@ -596,8 +598,8 @@ class StructPayload(PayloadBase[NpStructT]): Example:: class MyPayload(StructPayload[np.uint8]): - channel = _Field(UInt16Converter()) - enabled = _BitFlag(0x01, dtype=np.uint8) + channel = Field(converter=UInt16Converter()) + enabled = BitFlag(mask=0x01, dtype=np.uint8) """ diff --git a/tests/protocol/test_converter.py b/tests/protocol/test_converter.py index 19571f7..55b73f2 100644 --- a/tests/protocol/test_converter.py +++ b/tests/protocol/test_converter.py @@ -14,9 +14,9 @@ import pytest from harp.protocol._payload import ( PayloadBase, - _BitFlag, - _Field, - _GroupMask, + BitFlag, + Field, + GroupMask, _IdentityConverter, ) from harp.protocol._payload_converters import StringConverter as _StringConverter @@ -39,8 +39,8 @@ class _Color(enum.IntEnum): class _NumericPayload(PayloadBase): - a = _Field(converter=_IdentityConverter(" pd.DataFrame: return pd.DataFrame( diff --git a/tests/protocol/test_register.py b/tests/protocol/test_register.py index 118f4bf..c4f4bf1 100644 --- a/tests/protocol/test_register.py +++ b/tests/protocol/test_register.py @@ -17,7 +17,7 @@ PayloadU16, PayloadU32, PayloadU64, - _Field, + Field, _IdentityConverter, ) from harp.protocol._payload_type import PayloadType @@ -49,9 +49,9 @@ class DigitalOutputSet(RegisterU16): class AnalogDataPayload(PayloadBase): - analog_input0 = _Field(converter=_IdentityConverter(" Date: Sun, 14 Jun 2026 11:27:01 -0700 Subject: [PATCH 225/267] Generalize payload construction for all interface types --- scripts/benchmark_analog_data.py | 4 +- src/harp-device/src/harp/device/_registers.py | 38 +- src/harp-protocol/harp/protocol/__init__.py | 4 + src/harp-protocol/harp/protocol/_payload.py | 390 ++++++++++---- .../harp/protocol/_payload_converters.py | 46 ++ tests/protocol/register_models.py | 482 ++++++++++++++++++ tests/protocol/test_converter.py | 32 +- tests/protocol/test_payload.py | 4 +- tests/protocol/test_register.py | 16 +- tests/protocol/test_register_modeling.py | 208 ++++++++ tests/test_device.py | 22 +- 11 files changed, 1091 insertions(+), 155 deletions(-) create mode 100644 tests/protocol/register_models.py create mode 100644 tests/protocol/test_register_modeling.py diff --git a/scripts/benchmark_analog_data.py b/scripts/benchmark_analog_data.py index 5006e10..a9e5ec8 100644 --- a/scripts/benchmark_analog_data.py +++ b/scripts/benchmark_analog_data.py @@ -43,7 +43,7 @@ ANALOG_COLUMNS = ["analog_input0", "encoder", "analog_input1"] -class AnalogDataPayload(PayloadBase[np.void]): +class AnalogDataPayload(PayloadBase[np.int16]): """Payload for AnalogData (register 44): three signed 16-bit channels.""" analog_input0 = Field(converter=IdentityConverter(np.int16)) @@ -63,7 +63,7 @@ class AnalogData(RegisterBase[AnalogDataPayload]): # Helpers # --------------------------------------------------------------------------- -BIN_FILE = REPO_ROOT / "notes" / "Behavior.harp" / "Behavior_44.bin" +BIN_FILE = REPO_ROOT / "scripts" / "Behavior_44.bin" def pyharp_read(path: Path, *, include_timestamp: bool = True): diff --git a/src/harp-device/src/harp/device/_registers.py b/src/harp-device/src/harp/device/_registers.py index 81d0192..e26d26e 100644 --- a/src/harp-device/src/harp/device/_registers.py +++ b/src/harp-device/src/harp/device/_registers.py @@ -30,31 +30,31 @@ class EnableFlag(enum.IntEnum): class OperationControlPayload(StructPayload[np.uint8]): operation_mode: OperationMode = GroupMask( - mask=0x03, shift=0, enum=OperationMode, dtype=np.uint8, default=OperationMode.Standby + mask=0x03, enum=OperationMode, default=OperationMode.Standby ) - dump_registers: bool = BitFlag(mask=0x08, dtype=np.uint8, default=False) - mute_replies: bool = BitFlag(mask=0x10, dtype=np.uint8, default=False) + dump_registers: bool = BitFlag(mask=0x08, default=False) + mute_replies: bool = BitFlag(mask=0x10, default=False) visual_indicators: EnableFlag = GroupMask( - mask=0x20, shift=5, enum=EnableFlag, dtype=np.uint8, default=EnableFlag.Disabled + mask=0x20, enum=EnableFlag, default=EnableFlag.Disabled ) operation_led: EnableFlag = GroupMask( - mask=0x40, shift=6, enum=EnableFlag, dtype=np.uint8, default=EnableFlag.Disabled + mask=0x40, enum=EnableFlag, default=EnableFlag.Disabled ) heartbeat: EnableFlag = GroupMask( - mask=0x80, shift=7, enum=EnableFlag, dtype=np.uint8, default=EnableFlag.Disabled + mask=0x80, enum=EnableFlag, default=EnableFlag.Disabled ) class ResetDevicePayload(StructPayload[np.uint8]): """Payload for the ResetDevice register (address 11).""" - restore_default: bool = BitFlag(mask=0x01, dtype=np.uint8, default=False) - restore_eeprom: bool = BitFlag(mask=0x02, dtype=np.uint8, default=False) - save: bool = BitFlag(mask=0x04, dtype=np.uint8, default=False) - restore_name: bool = BitFlag(mask=0x08, dtype=np.uint8, default=False) - update_firmware: bool = BitFlag(mask=0x20, dtype=np.uint8, default=False) - boot_from_default: bool = BitFlag(mask=0x40, dtype=np.uint8, default=False) - boot_from_eeprom: bool = BitFlag(mask=0x80, dtype=np.uint8, default=False) + restore_default: bool = BitFlag(mask=0x01, default=False) + restore_eeprom: bool = BitFlag(mask=0x02, default=False) + save: bool = BitFlag(mask=0x04, default=False) + restore_name: bool = BitFlag(mask=0x08, default=False) + update_firmware: bool = BitFlag(mask=0x20, default=False) + boot_from_default: bool = BitFlag(mask=0x40, default=False) + boot_from_eeprom: bool = BitFlag(mask=0x80, default=False) class DeviceNamePayload(StructPayload[np.uint8]): @@ -71,12 +71,12 @@ class DeviceNamePayload(StructPayload[np.uint8]): class ClockConfigPayload(StructPayload[np.uint8]): """Payload for the ClockConfiguration register (address 14).""" - clock_repeater: bool = BitFlag(mask=0x01, dtype=np.uint8, default=False) - clock_generator: bool = BitFlag(mask=0x02, dtype=np.uint8, default=False) - repeater_capability: bool = BitFlag(mask=0x08, dtype=np.uint8, default=False) - generator_capability: bool = BitFlag(mask=0x10, dtype=np.uint8, default=False) - clock_unlock: bool = BitFlag(mask=0x40, dtype=np.uint8, default=False) - clock_lock: bool = BitFlag(mask=0x80, dtype=np.uint8, default=False) + clock_repeater: bool = BitFlag(mask=0x01, default=False) + clock_generator: bool = BitFlag(mask=0x02, default=False) + repeater_capability: bool = BitFlag(mask=0x08, default=False) + generator_capability: bool = BitFlag(mask=0x10, default=False) + clock_unlock: bool = BitFlag(mask=0x40, default=False) + clock_lock: bool = BitFlag(mask=0x80, default=False) # --------------------------------------------------------------------------- diff --git a/src/harp-protocol/harp/protocol/__init__.py b/src/harp-protocol/harp/protocol/__init__.py index 851946b..e4507d0 100644 --- a/src/harp-protocol/harp/protocol/__init__.py +++ b/src/harp-protocol/harp/protocol/__init__.py @@ -1,7 +1,9 @@ from ._message import HarpMessage, HarpParseError, ParsedHarpMessage from ._message_type import MessageType from ._payload_converters import ( + BoolConverter, Converter, + EnumConverter, IdentityConverter, StringConverter, register_converter, @@ -67,6 +69,8 @@ "Converter", "IdentityConverter", "StringConverter", + "BoolConverter", + "EnumConverter", "register_converter", # Payload DSL "PayloadBase", diff --git a/src/harp-protocol/harp/protocol/_payload.py b/src/harp-protocol/harp/protocol/_payload.py index cdcbfb8..de18c16 100644 --- a/src/harp-protocol/harp/protocol/_payload.py +++ b/src/harp-protocol/harp/protocol/_payload.py @@ -1,5 +1,16 @@ import enum -from typing import TYPE_CHECKING, Any, ClassVar, Generic, Protocol, TypeVar, final, overload +from dataclasses import dataclass +from typing import ( + TYPE_CHECKING, + Any, + ClassVar, + Generic, + Protocol, + TypeVar, + final, + get_args, + overload, +) import numpy as np import pandas as pd @@ -14,6 +25,23 @@ E = TypeVar("E", bound=enum.IntEnum) _MISSING = Sentinel("_MISSING") +_DEFAULT_ELEMENT = np.dtype(np.uint8) + + +@dataclass(frozen=True) +class _FieldSlot: + """One physical numpy field: its dtype and byte offset within the record.""" + + dtype: np.dtype + byte_offset: int + + +def _mask_trailing_zeros(mask: int) -> int: + """Number of trailing zero bits in ``mask`` — the right-shift that aligns a + masked field to bit 0.""" + if mask == 0: + return 0 + return (mask & -mask).bit_length() - 1 # --------------------------------------------------------------------------- @@ -22,19 +50,33 @@ class Field(Generic[T]): - """Descriptor for a payload field with a Converter. - Note: The name of the field is used as the slot name in the underlying numpy structured array. + """Descriptor for a whole-element (or reinterpreted multi-element) payload view. + + The view reads ``converter.dtype.itemsize`` bytes starting at ``offset`` (in + base-element units; see :class:`StructPayload`) and runs them through + ``converter``. The converter owns its own ``dtype`` (byte layout) and is + independent of the payload's base element type, so the same converter works + under any register width. + + ``offset`` defaults to ``0``. Omitting it suits a payload with a single + member; when a payload has several distinct slots, each must declare an + explicit ``offset=`` or the overlap check rejects the layout. """ if TYPE_CHECKING: # Makes `field: T = Field(converter=...)` valid under @dataclass_transform without a # type-mismatch error. At runtime __new__ is not defined and a Field instance is # returned as normal. - def __new__(cls, *, converter: "_Converter[T]", default: "T" = ...) -> "T": ... # type: ignore[misc] + def __new__( # type: ignore[misc] + cls, converter: "_Converter[T]", *, offset: int = 0, default: "T" = ... + ) -> "T": ... - def __init__(self, *, converter: _Converter[T], default: object = _MISSING) -> None: + def __init__( + self, converter: _Converter[T], *, offset: int = 0, default: object = _MISSING + ) -> None: self._converter = converter self._name: str | None = None + self._offset = offset self._default = default def __set_name__(self, owner: object, name: str) -> None: @@ -54,37 +96,27 @@ def _to_batch(self) -> "_FieldBatch[T]": class BitFlag: - """Descriptor for a bit flag within a payload field. - Notes: - 1) ensure dtype of the slot can hold the mask (e.g. uint8 for mask 0x01, uint16 for mask 0x100, etc.) - 2) An additional "slot" name can be provided if the bit flag is not stored in the same-named slot as the field descriptors (e.g. if multiple bit flags are packed into a single byte slot). + """Descriptor for a single bit within a payload element, exposed as ``bool``. + + Reads the base element at ``offset`` (base-element units; defaults to ``0``) + and tests ``element & mask``. The element width and the physical storage slot + are derived from the payload's base element type, so several bit flags at the + same offset automatically share storage. """ if TYPE_CHECKING: def __new__( - cls, - *, - mask: int, - slot: str = "value", - dtype: "np.dtype | str | type" = np.uint8, - default: bool = ..., + cls, *, mask: int, offset: int = 0, default: bool = ... ) -> bool: ... # type: ignore[misc] # noqa: E704 - def __init__( - self, - *, - mask: int, - slot: str = "value", - dtype: "np.dtype | str | type" = np.uint8, - default: object = _MISSING, - ) -> None: + def __init__(self, *, mask: int, offset: int = 0, default: object = _MISSING) -> None: self._mask = mask - self._slot = slot - self._dtype = np.dtype( - dtype - ) # TODO I am not sure we need this. In theory we could just assume it uses the word size of the Payload dtype and validate that the mask fits within it, rather than storing a separate dtype here. + self._offset = offset self._default = default + # Derived in PayloadBase.__init_subclass__: + self._slot: str = "value" + self._dtype: np.dtype = _DEFAULT_ELEMENT @overload def __get__(self, obj: None, owner: object = None) -> "BitFlag": ... @@ -112,42 +144,79 @@ def _build_enum_lookup(enum_cls: type) -> "tuple[list[str], np.ndarray]": class GroupMask(Generic[E]): - """Descriptor for a group of bits representing an enum value within a payload field. - Notes: - 1) ensure dtype of the slot can hold the mask (e.g. uint8 for mask 0x01, uint16 for mask 0x100, etc.) - 2) An additional "slot" name can be provided if the group mask is not stored in the same-named slot as the field descriptors (e.g. if multiple group masks are packed into a single byte slot). + """Descriptor for a masked, shifted sub-field of a payload element. + + The raw value is extracted as ``(element & mask) >> shift`` and then mapped: + + * ``enum=`` — to an ``enum.IntEnum`` member (strict; unknown code raises); + * ``converter=`` — through a :class:`Converter` applied to the *masked* + integer (numeric casts, bool, custom); + * neither — returned as the raw masked numpy integer (the element's dtype). + + The right-shift is always derived from ``mask`` (its trailing-zero count, so the + field aligns to bit 0); ``offset`` defaults to ``0``. The element width and + storage slot are derived from the payload's base element type, so several masked + fields at the same offset share storage automatically. """ if TYPE_CHECKING: - - def __new__( - cls, - *, - mask: int, - shift: int, - enum: "type[E]", - slot: str = "value", - dtype: "np.dtype | str | type" = np.uint8, - default: "E" = ..., - ) -> "E": ... # type: ignore[misc] # noqa: E704 + # enum variant -> the field type is the enum + @overload + def __new__( # type: ignore[misc] # noqa: E704 + cls, *, mask: int, enum: "type[E]", offset: int = 0, default: "E" = ... + ) -> "E": ... + # converter variant -> the field type is the converter's output type + @overload + def __new__( # type: ignore[misc] # noqa: E704 + cls, *, mask: int, converter: "_Converter[T]", offset: int = 0, default: "T" = ... + ) -> "T": ... + # raw variant -> a plain integer + @overload + def __new__( # type: ignore[misc] # noqa: E704 + cls, *, mask: int, offset: int = 0, default: int = ... + ) -> int: ... + def __new__(cls, **kwargs: Any) -> Any: ... # type: ignore[misc] # noqa: E704 def __init__( self, *, mask: int, - shift: int, - enum: type[E], - slot: str = "value", - dtype: "np.dtype | str | type" = np.uint8, + enum: type[E] | None = None, + converter: "_Converter[Any] | None" = None, + offset: int = 0, default: object = _MISSING, ) -> None: + if enum is not None and converter is not None: + raise TypeError("GroupMask accepts at most one of 'enum' or 'converter'") self._mask = mask - self._shift = shift + self._shift = _mask_trailing_zeros(mask) self._enum = enum - self._slot = slot - self._dtype = np.dtype(dtype) + self._converter = converter + self._offset = offset self._default = default - self._categories, self._code_lookup = _build_enum_lookup(enum) + # Derived in PayloadBase.__init_subclass__: + self._slot: str = "value" + self._dtype: np.dtype = _DEFAULT_ELEMENT + if enum is not None: + self._categories, self._code_lookup = _build_enum_lookup(enum) + else: + self._categories, self._code_lookup = None, None + + def _decode_raw(self, raw: Any) -> Any: + """Map an extracted (already masked + shifted) integer to its value.""" + if self._enum is not None: + return self._enum(int(raw)) + if self._converter is not None: + return self._converter.decode_scalar(self._converter.dtype.type(raw)) + return raw + + def _encode_value(self, value: Any) -> int: + """Map a user value back to the integer to be masked + shifted into the slot.""" + if self._converter is not None and self._enum is None: + tmp = np.zeros((), dtype=self._converter.dtype) + self._converter.encode_into(tmp, value) + return int(tmp) + return int(value) @overload def __get__(self, obj: None, owner: object = None) -> "GroupMask[E]": ... @@ -157,13 +226,13 @@ def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: if obj is None: return self raw = (obj._arr[self._slot] & self._mask) >> self._shift - return self._enum(int(raw)) + return self._decode_raw(raw) def _to_batch(self) -> "_GroupMaskBatch[E]": return _GroupMaskBatch( self._mask, - self._shift, self._enum, + converter=self._converter, slot=self._slot, dtype=self._dtype, ) @@ -220,23 +289,27 @@ def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: class _GroupMaskBatch(Generic[E]): - """Same as _GroupMask but returns an NDArray view for batch payloads rather than a scalar value.""" + """Same as GroupMask but returns an NDArray view for batch payloads rather than a scalar value.""" def __init__( self, mask: int, - shift: int, - enum: type[E], + enum: type[E] | None, *, + converter: "_Converter[Any] | None" = None, slot: str = "value", dtype: "np.dtype | str | type" = np.uint8, ) -> None: self._mask = mask - self._shift = shift + self._shift = _mask_trailing_zeros(mask) self._enum = enum + self._converter = converter self._slot = slot self._dtype = np.dtype(dtype) - self._categories, self._code_lookup = _build_enum_lookup(enum) + if enum is not None: + self._categories, self._code_lookup = _build_enum_lookup(enum) + else: + self._categories, self._code_lookup = None, None @overload def __get__(self, obj: None, owner: object = None) -> "_GroupMaskBatch[E]": ... @@ -245,7 +318,12 @@ def __get__(self, obj: "PayloadBase", owner: object = None) -> "NDArray[Any]": . def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: if obj is None: return self - return (obj._arr[self._slot] & self._mask) >> self._shift + raw = (obj._arr[self._slot] & self._mask) >> self._shift + # Enum batches return raw integer codes (see to_dataframe for Categorical + # decoding). Converter batches decode element-wise; raw stays integer. + if self._enum is None and self._converter is not None: + return self._converter.decode_batch(raw.astype(self._converter.dtype)) + return raw _PT = TypeVar("_PT", bound="PayloadBase[Any]") @@ -298,6 +376,109 @@ def _batch_init_disabled(self: "PayloadBase", *args: object, **kwargs: object) - ) +def _resolve_element_dtype(cls: type) -> np.dtype: + """Resolve the base element dtype from the ``StructPayload[...]`` type arg. + + Only used for offset→byte arithmetic and the masked-read integer width. + Defaults to uint8 (byte) when the payload is not parameterized, which makes + byte-offset layouts of heterogeneous consecutive fields work out of the box. + """ + for base in getattr(cls, "__orig_bases__", ()): + for arg in get_args(base): + if isinstance(arg, TypeVar): + continue + try: + return np.dtype(arg) + except TypeError: + continue + return _DEFAULT_ELEMENT + + +def _validate_mask_fits(cls: type, name: str, mask: int, elem: np.dtype) -> None: + if mask < 0 or mask >= (1 << (elem.itemsize * 8)): + raise TypeError( + f"{cls.__name__}: mask {mask:#x} on {name!r} does not fit the base " + f"element {elem} ({elem.itemsize} byte(s))" + ) + + +def _validate_no_overlap(cls: type, slots: "dict[str, _FieldSlot]", itemsize: int) -> None: + """Validate that declared fields do not overlap and fit within the payload itemsize. Overlap is only + allowed for masked fields sharing the same slot.""" + spans = sorted( + (slot.byte_offset, slot.byte_offset + slot.dtype.itemsize, name) + for name, slot in slots.items() + ) + for start, end, name in spans: + if end > itemsize: + raise TypeError( + f"{cls.__name__}: field {name!r} ends at byte {end}, beyond itemsize " + f"{itemsize}; declare an explicit length=" + ) + for (prev_start, prev_end, prev_name), (start, end, name) in zip(spans, spans[1:]): + if start < prev_end: + raise TypeError( + f"{cls.__name__}: fields {prev_name!r} and {name!r} overlap " + f"(bytes [{prev_start},{prev_end}) vs [{start},{end})); masked sub-fields " + f"of the same element must share an offset" + ) + + +def _build_struct_dtype( + cls: type, + declarations: "list[tuple[str, Field | BitFlag | GroupMask]]", + length: int | None, +) -> np.dtype: + """Build the numpy structured dtype from field declarations.""" + elem = cls._elem_dtype + elem_size = elem.itemsize + slots: dict[str, _FieldSlot] = {} + mask_slot_by_byte_offset: dict[int, str] = {} + + for attr_name, val in declarations: + byte_offset = val._offset * elem_size + if isinstance(val, (BitFlag, GroupMask)): + val._dtype = elem + _validate_mask_fits(cls, attr_name, val._mask, elem) + shared_slot = mask_slot_by_byte_offset.get(byte_offset) + if shared_slot is not None: + val._slot = shared_slot + else: + mask_slot_by_byte_offset[byte_offset] = attr_name + val._slot = attr_name + slots[attr_name] = _FieldSlot(elem, byte_offset) + else: # Field + field_dtype = val._converter.dtype + if attr_name in slots: + raise TypeError(f"{cls.__name__}: duplicate field name {attr_name!r}") + slots[attr_name] = _FieldSlot(field_dtype, byte_offset) + + if length is not None: + itemsize = length * elem_size + else: + itemsize = max(slot.byte_offset + slot.dtype.itemsize for slot in slots.values()) + _validate_no_overlap(cls, slots, itemsize) + return np.dtype( + { + "names": list(slots), + "formats": [slot.dtype for slot in slots.values()], + "offsets": [slot.byte_offset for slot in slots.values()], + "itemsize": itemsize, + } + ) + + +def _resolve_single_member(cls: type, declarations: "list[tuple[str, Any]]") -> "str | None": + """A payload with exactly one full-span ``Field`` unwraps to that member on + ``parse`` (register-level ``interfaceType``), avoiding a ``.value`` hop.""" + if len(declarations) != 1: + return None + attr, val = declarations[0] + if isinstance(val, Field) and val._converter.dtype.itemsize == cls.dtype.itemsize: # type: ignore[attr-defined] + return attr + return None + + class PayloadBase(Generic[NpStructT]): """Base class for typed Harp register payloads. @@ -323,6 +504,11 @@ class PayloadBase(Generic[NpStructT]): _defaults: ClassVar[dict[str, Any]] # Auto-generated sibling class whose descriptors return NDArray views instead of scalars. Batch: ClassVar["type[PayloadBase]"] + # Base element dtype (from the ``StructPayload[...]`` type arg); governs offset + # arithmetic and the integer width used for masked reads. Defaults to uint8. + _elem_dtype: ClassVar[np.dtype] = _DEFAULT_ELEMENT + # Attribute name of the lone full-span member, if any: ``parse`` unwraps to it. + _single_member: ClassVar[str | None] = None # The underlying numpy array holding one (0-D) or many (1-D) payload records. _arr: NDArray[NpStructT] @@ -349,35 +535,29 @@ def __init__(self, *args: object, **kwargs: object) -> None: merged.update(kwargs) kwargs = merged - slot_kwargs = {k: v for k, v in kwargs.items() if k in names} - descriptor_kwargs = {k: v for k, v in kwargs.items() if k not in names} - - bitfields = cls._bitfields - unknown = set(descriptor_kwargs) - set(bitfields) - if unknown: - raise TypeError(f"{cls.__name__}() got unexpected kwargs: {sorted(unknown)}") - arr = np.zeros((), dtype=self.dtype) - for name in names: - if name not in slot_kwargs: - continue - desc = cls._mro_descriptor(name) + # Route each kwarg by its descriptor kind, not by whether its name happens + # to match a numpy slot — masked descriptors may share a slot whose name + # collides with the first masked field's attribute name. + for attr_name, value in kwargs.items(): + desc = cls._mro_descriptor(attr_name) if isinstance(desc, _FIELD_TYPES): - desc._converter.encode_into(arr[name], slot_kwargs[name]) - else: - arr[name] = slot_kwargs[name] - - for attr_name, value in descriptor_kwargs.items(): - desc = bitfields[attr_name] - slot = desc._slot - mask_in_dtype = np.array(desc._mask, dtype=desc._dtype) - if isinstance(desc, _BIT_FLAG_TYPES): - if value: - arr[slot] |= mask_in_dtype + desc._converter.encode_into(arr[desc._name], value) + elif isinstance(desc, _BITFIELD_TYPES): + slot = desc._slot + mask_in_dtype = np.array(desc._mask, dtype=desc._dtype) + if isinstance(desc, _BIT_FLAG_TYPES): + if value: + arr[slot] |= mask_in_dtype + else: + int_val = desc._encode_value(value) + shifted = np.array((int_val << desc._shift) & desc._mask, dtype=desc._dtype) + arr[slot] = (arr[slot] & ~mask_in_dtype) | shifted + elif attr_name in names: + arr[attr_name] = value # raw slot with no descriptor else: - shifted = np.array((int(value) << desc._shift) & desc._mask, dtype=desc._dtype) - arr[slot] = (arr[slot] & ~mask_in_dtype) | shifted + raise TypeError(f"{cls.__name__}() got unexpected kwarg: {attr_name!r}") self._arr = arr @@ -412,6 +592,7 @@ def __init_subclass__( cls, *, _batch_of: "type[PayloadBase] | None" = None, + length: int | None = None, **kwargs: object, ) -> None: super().__init_subclass__(**kwargs) @@ -421,11 +602,16 @@ def __init_subclass__( # scalar twin and wire the scalar↔batch pointers. cls.dtype = _batch_of.dtype cls._repr_fields = _batch_of._repr_fields + cls._elem_dtype = _batch_of._elem_dtype + cls._single_member = _batch_of._single_member cls._scalar_cls = _batch_of cls._batch_cls = cls _batch_of._batch_cls = cls return + cls._elem_dtype = _resolve_element_dtype(cls) + cls._single_member = None + for name, val in cls.__dict__.items(): if isinstance(val, _DECLARATION_TYPES) and name in _RESERVED_FIELD_NAMES: raise TypeError(f"{cls.__name__}: field name {name!r} is reserved by PayloadBase") @@ -437,21 +623,8 @@ def __init_subclass__( ] if own_declarations: - slots: dict[str, np.dtype] = {} - for attr_name, val in own_declarations: - if isinstance(val, Field): - slot, dtype = val._name, val._converter.dtype - else: - slot, dtype = val._slot, val._dtype - if slot in slots: - if slots[slot] != dtype: - raise TypeError( - f"{cls.__name__}: slot {slot!r} declared with conflicting " - f"dtypes {slots[slot]} and {dtype}" - ) - else: - slots[slot] = dtype - cls.dtype = np.dtype(list(slots.items())) + cls.dtype = _build_struct_dtype(cls, own_declarations, length) + cls._single_member = _resolve_single_member(cls, own_declarations) if "_repr_fields" not in cls.__dict__: bitfield_names = tuple( @@ -513,9 +686,11 @@ def to_dataframe(self, *, decode_enums: bool = True) -> pd.DataFrame: if isinstance(desc, _GROUP_MASK_TYPES): slot_col = arr[desc._slot] raw = (slot_col & desc._mask) >> desc._shift - if decode_enums: + if desc._enum is not None and decode_enums: codes = desc._code_lookup[raw] cols[f] = pd.Categorical.from_codes(codes, categories=desc._categories) + elif desc._enum is None and desc._converter is not None: + cols[f] = desc._converter.decode_batch(raw.astype(desc._converter.dtype)) else: cols[f] = raw elif isinstance(desc, _BIT_FLAG_TYPES): @@ -574,8 +749,14 @@ def unwrap(cls, arr: "np.ndarray") -> Any: struct payloads (e.g. a struct with one uint16 field can just be a PayloadU16 subclass) while still supporting the full descriptor machinery for multi-field struct payloads. + + A struct payload with exactly one full-span member (register-level + ``interfaceType``) unwraps directly to that member's value. """ - return cls.from_array(arr) + obj = cls.from_array(arr) + if cls._single_member is not None and arr.ndim == 0: + return getattr(obj, cls._single_member) + return obj # --------------------------------------------------------------------------- @@ -595,11 +776,16 @@ class StructPayload(PayloadBase[NpStructT]): those declarations, so constructor calls are fully type-checked and have IDE autocompletion. + The type argument (``StructPayload[np.uint8]``) is the base element type; it + sets the unit for ``offset=`` and the integer width of masked reads. The + optional ``length=`` class kwarg fixes the payload size in base elements + (the register ``length``); when omitted it defaults to the max member extent. + Example:: class MyPayload(StructPayload[np.uint8]): - channel = Field(converter=UInt16Converter()) - enabled = BitFlag(mask=0x01, dtype=np.uint8) + channel: np.uint16 = Field(UInt16Converter(), offset=0) + enabled: bool = BitFlag(mask=0x01, offset=2) """ diff --git a/src/harp-protocol/harp/protocol/_payload_converters.py b/src/harp-protocol/harp/protocol/_payload_converters.py index bfd2bbc..c5c16fd 100644 --- a/src/harp-protocol/harp/protocol/_payload_converters.py +++ b/src/harp-protocol/harp/protocol/_payload_converters.py @@ -1,3 +1,4 @@ +import enum as _enum from abc import ABC, abstractmethod from collections.abc import Callable from typing import Any, Generic, TypeVar, cast @@ -7,6 +8,7 @@ T = TypeVar("T") NpScalarT = TypeVar("NpScalarT", bound=np.generic) +E = TypeVar("E", bound=_enum.IntEnum) _ConverterClsT = TypeVar("_ConverterClsT", bound="type[Converter[Any]]") # --------------------------------------------------------------------------- @@ -145,6 +147,50 @@ def __init__(self) -> None: super().__init__(np.float32) +class BoolConverter(Converter[bool]): + """Whole-element ``interfaceType: bool`` (distinct from a single ``BitFlag`` bit). + + The element is non-zero → ``True``. Operates on a single base element. + """ + + init_kwarg_type = bool + + def __init__(self, dtype: "np.dtype | str | type" = np.uint8) -> None: + self.dtype = np.dtype(dtype) + + def decode_scalar(self, view: np.generic) -> bool: + return bool(view) + + def decode_batch(self, view: NDArray[np.generic]) -> Any: + return np.asarray(view) != 0 + + def encode_into(self, view: NDArray[np.generic], value: bool) -> None: + view[...] = 1 if value else 0 + + +class EnumConverter(Converter[E]): + """Whole-element ``interfaceType: `` enum (strict). + + Maps a base element to an ``enum.IntEnum`` member; an unknown code raises + ``ValueError`` (matching Python ``IntEnum`` semantics). For masked enum + sub-fields use :class:`~harp.protocol.GroupMask` with ``enum=`` instead. + """ + + def __init__(self, enum_cls: "type[E]", dtype: "np.dtype | str | type" = np.uint8) -> None: + self._enum = enum_cls + self.dtype = np.dtype(dtype) + self.init_kwarg_type = enum_cls + + def decode_scalar(self, view: np.generic) -> E: + return self._enum(int(view)) + + def decode_batch(self, view: NDArray[np.generic]) -> Any: + return np.asarray(view) + + def encode_into(self, view: NDArray[np.generic], value: E) -> None: + view[...] = int(value) + + @register_converter(name="string") # TODO check this name against Bonsai class StringConverter(Converter[str]): """Converts a fixed-length byte array to/from a Python ``str``.""" diff --git a/tests/protocol/register_models.py b/tests/protocol/register_models.py new file mode 100644 index 0000000..c0a35ba --- /dev/null +++ b/tests/protocol/register_models.py @@ -0,0 +1,482 @@ +"""Reference models for every register in the canonical generator test device +(``harp-tech/generators`` -> ``tests/Metadata/device.yml``), built with the +``harp.protocol`` public API. + +These act as fixtures for ``test_register_modeling.py`` and demonstrate that the +API can express every payload shape the Harp protocol allows (gaps, overlaps, +masks, reinterpreted sub-regions, custom domain types) before building a +spec->code generator. Run it directly to round-trip every register:: + + uv run python -m tests.protocol.register_models + +Design (see notes/payload_api_redesign.md): + +* A payload is a flat buffer of base-``type`` elements; each member is a typed + view ``(offset, mask, converter)``. ``offset`` is in base-element units. +* ``Converter`` instances operate on their own byte layout and are independent of + the register element type (custom codecs read raw ``uint8`` sub-arrays), so the + same ``HarpVersionConverter`` works under a U8 or a U32 register. +* Masked sub-fields use ``GroupMask`` (enum / converter / raw); the right-shift is + derived from the mask's trailing zeros. +* The register ``length`` (base elements) fixes ``itemsize`` so byte gaps survive. +* Enum decoding is strict (an out-of-range code raises) — a deliberate divergence + from the C# generator's unchecked cast. +""" + +from __future__ import annotations + +import enum +from dataclasses import dataclass +from typing import Any, ClassVar + +import numpy as np +from numpy.typing import NDArray + +from harp.protocol import ( + BitFlag, + BoolConverter, + Converter, + Field, + GroupMask, + HarpMessage, + IdentityConverter, + PayloadType, + RegisterBase, + RegisterS32, + RegisterU8, + RegisterU16, + StringConverter, + StructPayload, +) + +# =========================================================================== +# device.yml bitMasks + groupMasks +# =========================================================================== + + +class PortDigitalIOS(enum.IntFlag): + """device.yml bitMasks.PortDigitalIOS (bits up to 0x800 — see PortDIOSet).""" + + DIO0 = 0x1 + DIO1 = 0x2 + DIO2 = 0x4 + DIO3 = 0x8 + DIPort0 = 0x100 + TestDIPort1 = 0x200 + SupplyPort0 = 0x400 + PortDIO1 = 0x800 + + +class PwmPort(enum.IntEnum): + """device.yml groupMasks.PwmPort (note Pwm3 = 0xA).""" + + Pwm0 = 0x1 + Pwm1 = 0x2 + Pwm2 = 0x4 + Pwm3 = 0xA + + +class EncoderModeMask(enum.IntEnum): + """device.yml groupMasks.EncoderModeMask.""" + + Position = 0x0 + Displacement = 0x1 + + +# =========================================================================== +# Custom interfaceType converters — byte-based, register-element-agnostic. +# =========================================================================== + + +@dataclass(frozen=True) +class HarpVersion: + """Illustrative domain type for ``interfaceType: HarpVersion`` (3 components).""" + + major: int + minor: int + patch: int + + def __str__(self) -> str: # pragma: no cover - cosmetic + return f"{self.major}.{self.minor}.{self.patch}" + + +class HarpVersionConverter(Converter[HarpVersion]): + """3 components <-> HarpVersion. Parameterized by component width, *not* by the + register: ``HarpVersionConverter(np.uint8)`` reads 3 bytes (Version members), + ``HarpVersionConverter(np.uint32)`` reads 12 bytes (CustomPayload register).""" + + init_kwarg_type = HarpVersion + + def __init__(self, component: "np.dtype | str | type" = np.uint8) -> None: + self.dtype = np.dtype((component, (3,))) + + def decode_scalar(self, view: np.generic) -> HarpVersion: + c = np.asarray(view).tolist() + return HarpVersion(int(c[0]), int(c[1]), int(c[2])) + + def decode_batch(self, view: NDArray[np.generic]) -> Any: + return np.array( + [HarpVersion(int(r[0]), int(r[1]), int(r[2])) for r in np.atleast_2d(view)], + dtype=object, + ) + + def encode_into(self, view: NDArray[np.generic], value: HarpVersion) -> None: + view[...] = np.array([value.major, value.minor, value.patch], dtype=self.dtype.base) + + +class BytesToIntConverter(Converter[int]): + """N raw bytes (little-endian) <-> Python int. Models ``interfaceType: int`` over a sub-array.""" + + init_kwarg_type = int + + def __init__(self, length: int, *, signed: bool = False) -> None: + self._length = length + self._signed = signed + self.dtype = np.dtype((np.uint8, (length,))) + + def decode_scalar(self, view: np.generic) -> int: + return int.from_bytes(bytes(np.asarray(view).tolist()), "little", signed=self._signed) + + def decode_batch(self, view: NDArray[np.generic]) -> Any: + return np.array( + [ + int.from_bytes(bytes(np.asarray(r).tolist()), "little", signed=self._signed) + for r in np.atleast_2d(view) + ], + dtype=object, + ) + + def encode_into(self, view: NDArray[np.generic], value: int) -> None: + view[...] = np.frombuffer( + int(value).to_bytes(self._length, "little", signed=self._signed), dtype=np.uint8 + ) + + +# =========================================================================== +# 32 DigitalInputs : U8, Event -> trivial scalar register +# =========================================================================== + + +class DigitalInputs(RegisterU8): + address: ClassVar[int] = 32 + + +# =========================================================================== +# 33 AnalogData : Float[6], Event — named sub-views + a 3-float sub-array. +# =========================================================================== + + +class AnalogDataPayload(StructPayload[np.float32], length=6): + Analog0: np.float32 = Field(IdentityConverter(np.float32), offset=0) + Analog1: np.float32 = Field(IdentityConverter(np.float32), offset=1) + Analog2: np.float32 = Field(IdentityConverter(np.float32), offset=2) + Accelerometer: NDArray[np.float32] = Field( + IdentityConverter(np.dtype((np.float32, (3,)))), offset=3 + ) + + +class AnalogData(RegisterBase[AnalogDataPayload]): + address: ClassVar[int] = 33 + payload_type: ClassVar[PayloadType] = PayloadType.Float + payload_class = AnalogDataPayload + + +# =========================================================================== +# 34 ComplexConfiguration : U8[17], Write — byte gap at bytes 1..3. +# =========================================================================== + + +class ComplexConfigurationPayload(StructPayload[np.uint8], length=17): + PwmPort: PwmPort = GroupMask(enum=PwmPort, mask=0xFF, offset=0) + DutyCycle: np.float32 = Field(IdentityConverter(np.float32), offset=4) + Frequency: np.float32 = Field(IdentityConverter(np.float32), offset=8) + EventsEnabled: bool = Field(BoolConverter(), offset=12) + Delta: np.uint32 = Field(IdentityConverter(np.uint32), offset=13) + _repr_fields = ("PwmPort", "DutyCycle", "Frequency", "EventsEnabled", "Delta") + + +class ComplexConfiguration(RegisterBase[ComplexConfigurationPayload]): + address: ClassVar[int] = 34 + payload_type: ClassVar[PayloadType] = PayloadType.U8 + payload_class = ComplexConfigurationPayload + + +# =========================================================================== +# 35 Version : U8[32], Event — HarpVersion x3 (3-byte) + string + raw hash. +# =========================================================================== + + +class VersionPayload(StructPayload[np.uint8], length=32): + ProtocolVersion: HarpVersion = Field(HarpVersionConverter(np.uint8), offset=0) + FirmwareVersion: HarpVersion = Field(HarpVersionConverter(np.uint8), offset=3) + HardwareVersion: HarpVersion = Field(HarpVersionConverter(np.uint8), offset=6) + CoreId: str = Field(StringConverter(3), offset=9) + InterfaceHash: NDArray[np.uint8] = Field( + IdentityConverter(np.dtype((np.uint8, (20,)))), offset=12 + ) + + +class Version(RegisterBase[VersionPayload]): + address: ClassVar[int] = 35 + payload_type: ClassVar[PayloadType] = PayloadType.U8 + payload_class = VersionPayload + + +# =========================================================================== +# 36 / 37 CustomPayload / CustomRawPayload : U32[3] — register-level +# interfaceType HarpVersion. Single full-span member -> parse() unwraps. +# =========================================================================== + + +class CustomPayloadPayload(StructPayload[np.uint32], length=3): + value: HarpVersion = Field(HarpVersionConverter(np.uint32)) + + +class CustomPayload(RegisterBase[HarpVersion]): + address: ClassVar[int] = 36 + payload_type: ClassVar[PayloadType] = PayloadType.U32 + payload_class = CustomPayloadPayload + + +class CustomRawPayloadPayload(StructPayload[np.uint32], length=3): + value: HarpVersion = Field(HarpVersionConverter(np.uint32)) + + +class CustomRawPayload(RegisterBase[HarpVersion]): + address: ClassVar[int] = 37 + payload_type: ClassVar[PayloadType] = PayloadType.U32 + payload_class = CustomRawPayloadPayload + + +# =========================================================================== +# 38 CustomMemberConverter : U8[3], Read — Header (uint8) + Data (2 bytes -> int). +# =========================================================================== + + +class CustomMemberConverterPayload(StructPayload[np.uint8], length=3): + Header: np.uint8 = Field(IdentityConverter(np.uint8)) + Data: int = Field(BytesToIntConverter(2, signed=True), offset=1) + + +class CustomMemberConverter(RegisterBase[CustomMemberConverterPayload]): + address: ClassVar[int] = 38 + payload_type: ClassVar[PayloadType] = PayloadType.U8 + payload_class = CustomMemberConverterPayload + + +# =========================================================================== +# 39 BitmaskSplitter : U8, Write — Low (mask 0xF, int) + High (mask 0xF0, int). +# =========================================================================== + + +class BitmaskSplitterPayload(StructPayload[np.uint8]): + Low: np.int32 = GroupMask(mask=0x0F, converter=IdentityConverter(np.int32)) + High: np.int32 = GroupMask(mask=0xF0, converter=IdentityConverter(np.int32)) + + +class BitmaskSplitter(RegisterBase[BitmaskSplitterPayload]): + address: ClassVar[int] = 39 + payload_type: ClassVar[PayloadType] = PayloadType.U8 + payload_class = BitmaskSplitterPayload + + +# =========================================================================== +# 40 Counter0 : S32, Event -> trivial scalar register +# =========================================================================== + + +class Counter0(RegisterS32): + address: ClassVar[int] = 40 + + +# =========================================================================== +# 41 PortDIOSet : U8, Write — bitMask PortDigitalIOS. Bits >= 0x100 do not fit a +# U8 payload; the C# generator truncates them too, so only DIO0..DIO3 model. +# =========================================================================== + + +class PortDIOSetPayload(StructPayload[np.uint8]): + DIO0: bool = BitFlag(mask=0x1) + DIO1: bool = BitFlag(mask=0x2) + DIO2: bool = BitFlag(mask=0x4) + DIO3: bool = BitFlag(mask=0x8) + + +class PortDIOSet(RegisterBase[PortDIOSetPayload]): + address: ClassVar[int] = 41 + payload_type: ClassVar[PayloadType] = PayloadType.U8 + payload_class = PortDIOSetPayload + + +# =========================================================================== +# 42 / 43 PulseDOPort0 / PulseDO0 : U16, Write +# =========================================================================== + + +class PulseDOPort0(RegisterU16): + address: ClassVar[int] = 42 + + +class PulseDO0(RegisterU16): + address: ClassVar[int] = 43 + + +# =========================================================================== +# 100 StartPulse : U16, Write — two overlapping views of one word. +# =========================================================================== + + +class StartPulsePayload(StructPayload[np.uint16]): + DigitalOutput: PwmPort = GroupMask(enum=PwmPort, mask=0xC00) + PulseWidth: np.uint16 = GroupMask(mask=0x3FF) + + +class StartPulse(RegisterBase[StartPulsePayload]): + address: ClassVar[int] = 100 + payload_type: ClassVar[PayloadType] = PayloadType.U16 + payload_class = StartPulsePayload + + +# =========================================================================== +# 101 StartPulseTrain : U16[2], Write — 4 masked members across two words. +# =========================================================================== + + +class StartPulseTrainPayload(StructPayload[np.uint16], length=2): + DigitalOutput: PwmPort = GroupMask(enum=PwmPort, mask=0xC00, offset=0) + PulseWidth: np.uint16 = GroupMask(mask=0x3FF, offset=0) + Frequency: np.uint8 = GroupMask( + mask=0xFF00, converter=IdentityConverter(np.uint8), offset=1, default=1 + ) + PulseCount: np.uint8 = GroupMask(mask=0xFF, converter=IdentityConverter(np.uint8), offset=1) + + +class StartPulseTrain(RegisterBase[StartPulseTrainPayload]): + address: ClassVar[int] = 101 + payload_type: ClassVar[PayloadType] = PayloadType.U16 + payload_class = StartPulseTrainPayload + + +# =========================================================================== +# 103 EncoderMode : U8, Write — whole-register groupMask. +# =========================================================================== + + +class EncoderModePayload(StructPayload[np.uint8]): + Mode: EncoderModeMask = GroupMask(enum=EncoderModeMask, mask=0xFF) + + +class EncoderMode(RegisterBase[EncoderModePayload]): + address: ClassVar[int] = 103 + payload_type: ClassVar[PayloadType] = PayloadType.U8 + payload_class = EncoderModePayload + + +# =========================================================================== +# Round-trip smoke test +# =========================================================================== + + +def _roundtrip(register: type[RegisterBase[Any]], value: Any) -> Any: + frame = register.format(value) + return register.parse(HarpMessage.parse(frame)) + + +def main() -> None: # pragma: no cover - manual exploration entry point + print("=== Every device.yml register, format -> parse round-trip ===\n") + + assert _roundtrip(DigitalInputs, np.uint8(0b1010)) == 0b1010 + print("DigitalInputs OK") + + ad = AnalogDataPayload( + Analog0=np.float32(1.0), + Analog1=np.float32(2.0), + Analog2=np.float32(3.0), + Accelerometer=np.array([4, 5, 6], dtype=np.float32), + ) + p = _roundtrip(AnalogData, ad) + assert float(p.Analog0) == 1.0 and float(p.Analog2) == 3.0 + np.testing.assert_array_equal(p.Accelerometer, [4, 5, 6]) + print(f"AnalogData OK ({AnalogDataPayload.dtype.itemsize} bytes)") + + cc = ComplexConfigurationPayload( + PwmPort=PwmPort.Pwm2, + DutyCycle=np.float32(0.5), + Frequency=np.float32(1000.0), + EventsEnabled=True, + Delta=np.uint32(42), + ) + p = _roundtrip(ComplexConfiguration, cc) + assert p.PwmPort == PwmPort.Pwm2 and p.EventsEnabled is True and int(p.Delta) == 42 + assert float(p.DutyCycle) == 0.5 + assert ComplexConfigurationPayload.dtype.itemsize == 17 + assert cc.raw_payload.tobytes()[1:4] == b"\x00\x00\x00" + print(f"ComplexConfiguration OK ({ComplexConfigurationPayload.dtype.itemsize} bytes, gap 1..3)") + + ver = VersionPayload( + ProtocolVersion=HarpVersion(2, 0, 0), + FirmwareVersion=HarpVersion(1, 2, 3), + HardwareVersion=HarpVersion(1, 0, 0), + CoreId="abc", + InterfaceHash=np.arange(20, dtype=np.uint8), + ) + p = _roundtrip(Version, ver) + assert p.ProtocolVersion == HarpVersion(2, 0, 0) and p.CoreId == "abc" + np.testing.assert_array_equal(p.InterfaceHash, np.arange(20)) + print(f"Version OK ({VersionPayload.dtype.itemsize} bytes)") + + p = _roundtrip(CustomPayload, CustomPayloadPayload(value=HarpVersion(3, 1, 4))) + assert p == HarpVersion(3, 1, 4) # single-member unwrap -> bare HarpVersion + p = _roundtrip(CustomRawPayload, CustomRawPayloadPayload(value=HarpVersion(0, 0, 1))) + assert p == HarpVersion(0, 0, 1) + print("CustomPayload/RawPayload OK (single-member unwrap)") + + p = _roundtrip(CustomMemberConverter, CustomMemberConverterPayload(Header=np.uint8(7), Data=-1234)) + assert int(p.Header) == 7 and int(p.Data) == -1234 + print("CustomMemberConverter OK") + + p = _roundtrip(BitmaskSplitter, BitmaskSplitterPayload(Low=0xA, High=0x5)) + assert int(p.Low) == 0xA and int(p.High) == 0x5 + assert p.raw_payload.tobytes() == bytes([0x5A]) + print("BitmaskSplitter OK") + + assert int(_roundtrip(Counter0, np.int32(-100000))) == -100000 + print("Counter0 OK") + + p = _roundtrip(PortDIOSet, PortDIOSetPayload(DIO0=True, DIO3=True)) + assert p.DIO0 is True and p.DIO3 is True and p.DIO1 is False + assert PortDIOSetPayload.dtype.itemsize == 1 + print("PortDIOSet OK") + + assert int(_roundtrip(PulseDOPort0, np.uint16(5))) == 5 + assert int(_roundtrip(PulseDO0, np.uint16(9))) == 9 + print("PulseDOPort0 / PulseDO0 OK") + + p = _roundtrip(StartPulse, StartPulsePayload(DigitalOutput=PwmPort.Pwm1, PulseWidth=np.uint16(300))) + assert p.DigitalOutput == PwmPort.Pwm1 and int(p.PulseWidth) == 300 + print("StartPulse OK") + + p = _roundtrip( + StartPulseTrain, + StartPulseTrainPayload( + DigitalOutput=PwmPort.Pwm1, + PulseWidth=np.uint16(300), + Frequency=np.uint8(200), + PulseCount=np.uint8(50), + ), + ) + assert p.DigitalOutput == PwmPort.Pwm1 and int(p.PulseWidth) == 300 + assert int(p.Frequency) == 200 and int(p.PulseCount) == 50 + assert StartPulseTrainPayload.dtype.itemsize == 4 + assert int(StartPulseTrainPayload(PulseCount=np.uint8(3)).Frequency) == 1 # defaultValue + print("StartPulseTrain OK (4 masked members, 2 words, default Frequency=1)") + + p = _roundtrip(EncoderMode, EncoderModePayload(Mode=EncoderModeMask.Displacement)) + assert p.Mode == EncoderModeMask.Displacement + print("EncoderMode OK") + + print("\nAll device.yml registers round-trip cleanly.") + + +if __name__ == "__main__": + main() diff --git a/tests/protocol/test_converter.py b/tests/protocol/test_converter.py index 55b73f2..0f8b718 100644 --- a/tests/protocol/test_converter.py +++ b/tests/protocol/test_converter.py @@ -39,8 +39,8 @@ class _Color(enum.IntEnum): class _NumericPayload(PayloadBase): - a = Field(converter=_IdentityConverter(" parse) +# --------------------------------------------------------------------------- + + +def test_scalar_registers_roundtrip(): + assert int(_roundtrip(DigitalInputs, np.uint8(0b1010))) == 0b1010 + assert int(_roundtrip(Counter0, np.int32(-100000))) == -100000 + assert int(_roundtrip(PulseDOPort0, np.uint16(5))) == 5 + assert int(_roundtrip(PulseDO0, np.uint16(9))) == 9 + + +def test_analog_data_roundtrip(): + ad = AnalogDataPayload( + Analog0=np.float32(1.0), + Analog1=np.float32(2.0), + Analog2=np.float32(3.0), + Accelerometer=np.array([4, 5, 6], dtype=np.float32), + ) + p = _roundtrip(AnalogData, ad) + assert float(p.Analog0) == 1.0 and float(p.Analog2) == 3.0 + np.testing.assert_array_equal(p.Accelerometer, [4, 5, 6]) + assert AnalogDataPayload.dtype.itemsize == 24 # 6 floats + + +def test_version_roundtrip(): + ver = VersionPayload( + ProtocolVersion=HarpVersion(2, 0, 0), + FirmwareVersion=HarpVersion(1, 2, 3), + HardwareVersion=HarpVersion(1, 0, 0), + CoreId="abc", + InterfaceHash=np.arange(20, dtype=np.uint8), + ) + p = _roundtrip(Version, ver) + assert p.ProtocolVersion == HarpVersion(2, 0, 0) + assert p.CoreId == "abc" + np.testing.assert_array_equal(p.InterfaceHash, np.arange(20)) + assert VersionPayload.dtype.itemsize == 32 + + +def test_custom_member_converter_roundtrip(): + p = _roundtrip(CustomMemberConverter, CustomMemberConverterPayload(Header=np.uint8(7), Data=-1234)) + assert int(p.Header) == 7 and int(p.Data) == -1234 + + +def test_encoder_mode_roundtrip(): + p = _roundtrip(EncoderMode, EncoderModePayload(Mode=EncoderModeMask.Displacement)) + assert p.Mode == EncoderModeMask.Displacement + assert isinstance(p.Mode, EncoderModeMask) + + +# --------------------------------------------------------------------------- +# Offsets + gaps (ComplexConfiguration) +# --------------------------------------------------------------------------- + + +def test_complex_configuration_gap_and_offsets(): + cc = ComplexConfigurationPayload( + PwmPort=PwmPort.Pwm2, + DutyCycle=np.float32(0.5), + Frequency=np.float32(1000.0), + EventsEnabled=True, + Delta=np.uint32(42), + ) + # itemsize from the register length (17), not the member extent. + assert ComplexConfigurationPayload.dtype.itemsize == 17 + # bytes 1..3 are an uncovered gap, preserved on encode. + assert cc.raw_payload.tobytes()[1:4] == b"\x00\x00\x00" + # explicit byte offsets (base element = uint8, so element units == bytes). + fields = ComplexConfigurationPayload.dtype.fields + assert fields["DutyCycle"][1] == 4 + assert fields["Delta"][1] == 13 + + p = _roundtrip(ComplexConfiguration, cc) + assert p.PwmPort == PwmPort.Pwm2 + assert float(p.DutyCycle) == 0.5 + assert p.EventsEnabled is True + assert int(p.Delta) == 42 + + +# --------------------------------------------------------------------------- +# Masked overlap on one element (StartPulse / StartPulseTrain / BitmaskSplitter) +# --------------------------------------------------------------------------- + + +def test_start_pulse_overlapping_masks(): + # Two views of one U16 element share storage (one numpy field, itemsize 2). + assert StartPulsePayload.dtype.itemsize == 2 + assert len(StartPulsePayload.dtype.names) == 1 + p = _roundtrip(StartPulse, StartPulsePayload(DigitalOutput=PwmPort.Pwm1, PulseWidth=np.uint16(300))) + assert p.DigitalOutput == PwmPort.Pwm1 + assert int(p.PulseWidth) == 300 + + +def test_start_pulse_train_two_words_and_default(): + p = _roundtrip( + StartPulseTrain, + StartPulseTrainPayload( + DigitalOutput=PwmPort.Pwm1, + PulseWidth=np.uint16(300), + Frequency=np.uint8(200), + PulseCount=np.uint8(50), + ), + ) + assert p.DigitalOutput == PwmPort.Pwm1 and int(p.PulseWidth) == 300 + assert int(p.Frequency) == 200 and int(p.PulseCount) == 50 + assert StartPulseTrainPayload.dtype.itemsize == 4 # two U16 words + # defaultValue: Frequency defaults to 1 when not provided. + assert int(StartPulseTrainPayload(PulseCount=np.uint8(3)).Frequency) == 1 + + +def test_bitmask_splitter_masked_ints(): + p = _roundtrip(BitmaskSplitter, BitmaskSplitterPayload(Low=0xA, High=0x5)) + assert int(p.Low) == 0xA and int(p.High) == 0x5 + assert p.raw_payload.tobytes() == bytes([0x5A]) # High packs into the top nibble + + +def test_port_dio_set_bitflags(): + p = _roundtrip(PortDIOSet, PortDIOSetPayload(DIO0=True, DIO3=True)) + assert p.DIO0 is True and p.DIO3 is True and p.DIO1 is False + assert PortDIOSetPayload.dtype.itemsize == 1 + + +# --------------------------------------------------------------------------- +# Register-level interfaceType: single full-span member unwraps on parse +# --------------------------------------------------------------------------- + + +def test_custom_payload_single_member_unwrap(): + assert CustomPayloadPayload._single_member == "value" + # U32[3] HarpVersion -> 12-byte buffer (3 x u32), same converter class. + assert CustomPayloadPayload.dtype.itemsize == 12 + parsed = _roundtrip(CustomPayload, CustomPayloadPayload(value=HarpVersion(3, 1, 4))) + assert isinstance(parsed, HarpVersion) + assert parsed == HarpVersion(3, 1, 4) + + +# --------------------------------------------------------------------------- +# Strict enums: an out-of-range masked code raises (divergence from C#) +# --------------------------------------------------------------------------- + + +def test_strict_enum_raises_on_unknown_code(): + # StartPulse.DigitalOutput is a 2-bit field; code 0b11 has no PwmPort member. + raw = np.array(0b11 << 10, dtype=np.uint16).tobytes() + payload = StartPulsePayload.from_buffer(raw) + with pytest.raises(ValueError): + _ = payload.DigitalOutput + + +# --------------------------------------------------------------------------- +# to_dataframe over masked + offset payloads +# --------------------------------------------------------------------------- + + +def test_complex_configuration_to_dataframe(): + cc = ComplexConfigurationPayload( + PwmPort=PwmPort.Pwm2, DutyCycle=np.float32(0.5), Frequency=np.float32(1.0), + EventsEnabled=True, Delta=np.uint32(42), + ) + batch = ComplexConfigurationPayload.from_buffer(cc.raw_payload.tobytes() * 2) + df = batch.to_dataframe() + assert len(df) == 2 + assert list(df["PwmPort"]) == ["Pwm2", "Pwm2"] + np.testing.assert_array_equal(df["Delta"], [42, 42]) diff --git a/tests/test_device.py b/tests/test_device.py index d3d8d43..e2dc451 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -36,7 +36,7 @@ class _FlagPayload(PayloadBase[np.uint8]): _repr_fields: ClassVar = ("flag", "group") flag = BitFlag(mask=0x01) - group = GroupMask(mask=0x06, shift=1, enum=OperationMode) + group = GroupMask(mask=0x06, enum=OperationMode) # --- BitFlag behaviour ------------------------------------------------------ @@ -175,22 +175,30 @@ def _make_frames(values: list, base_time: float = 1.0) -> bytes: return frames +def _read_frames(raw: bytes): + """Adapter over the current bulk API: returns (timestamps, payload).""" + _data, timestamps, _msgtype, payload = OperationControl.parse_bulk(raw) + if timestamps is None: + timestamps = np.empty(0, dtype=np.float64) + return timestamps, payload + + def test_read_frames_count(): raw = _make_frames([0x01, 0x00, 0x81]) - timestamps, payload = OperationControl.read_frames(raw) + timestamps, payload = _read_frames(raw) assert len(timestamps) == 3 assert len(payload) == 3 def test_read_frames_timestamps(): raw = _make_frames([0x01, 0x00, 0x81], base_time=10.0) - timestamps, _ = OperationControl.read_frames(raw) + timestamps, _ = _read_frames(raw) np.testing.assert_allclose(timestamps, [10.0, 11.0, 12.0], atol=1e-4) def test_read_frames_payload_type(): raw = _make_frames([0x01]) - _, payload = OperationControl.read_frames(raw) + _, payload = _read_frames(raw) assert isinstance(payload, OperationControlPayload) @@ -200,7 +208,7 @@ def test_read_frames_bitfield_batch(): _make_op_ctrl_byte(OperationMode.Standby, EnableFlag.Disabled), ] raw = _make_frames(vals) - _, payload = OperationControl.read_frames(raw) + _, payload = _read_frames(raw) np.testing.assert_array_equal(payload.heartbeat, [True, False]) np.testing.assert_array_equal( payload.operation_mode, @@ -211,7 +219,7 @@ def test_read_frames_bitfield_batch(): def test_read_frames_to_dataframe(): vals = [_make_op_ctrl_byte(OperationMode.Active), _make_op_ctrl_byte(), _make_op_ctrl_byte()] raw = _make_frames(vals) - _, payload = OperationControl.read_frames(raw) + _, payload = _read_frames(raw) df = payload.to_dataframe() assert len(df) == 3 assert "heartbeat" in df.columns @@ -219,6 +227,6 @@ def test_read_frames_to_dataframe(): def test_read_frames_empty(): - timestamps, payload = OperationControl.read_frames(b"") + timestamps, payload = _read_frames(b"") assert len(timestamps) == 0 assert len(payload) == 0 From afa9288fb1980ad06de64227897dbb0aad84ee95 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sun, 14 Jun 2026 11:28:11 -0700 Subject: [PATCH 226/267] Linting --- src/harp-device/src/harp/device/_registers.py | 8 ++------ src/harp-protocol/harp/protocol/_payload.py | 4 +--- tests/protocol/register_models.py | 12 +++++++++--- tests/protocol/test_register_modeling.py | 15 +++++++++++---- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/harp-device/src/harp/device/_registers.py b/src/harp-device/src/harp/device/_registers.py index e26d26e..4c18f79 100644 --- a/src/harp-device/src/harp/device/_registers.py +++ b/src/harp-device/src/harp/device/_registers.py @@ -37,12 +37,8 @@ class OperationControlPayload(StructPayload[np.uint8]): visual_indicators: EnableFlag = GroupMask( mask=0x20, enum=EnableFlag, default=EnableFlag.Disabled ) - operation_led: EnableFlag = GroupMask( - mask=0x40, enum=EnableFlag, default=EnableFlag.Disabled - ) - heartbeat: EnableFlag = GroupMask( - mask=0x80, enum=EnableFlag, default=EnableFlag.Disabled - ) + operation_led: EnableFlag = GroupMask(mask=0x40, enum=EnableFlag, default=EnableFlag.Disabled) + heartbeat: EnableFlag = GroupMask(mask=0x80, enum=EnableFlag, default=EnableFlag.Disabled) class ResetDevicePayload(StructPayload[np.uint8]): diff --git a/src/harp-protocol/harp/protocol/_payload.py b/src/harp-protocol/harp/protocol/_payload.py index de18c16..fafd721 100644 --- a/src/harp-protocol/harp/protocol/_payload.py +++ b/src/harp-protocol/harp/protocol/_payload.py @@ -106,9 +106,7 @@ class BitFlag: if TYPE_CHECKING: - def __new__( - cls, *, mask: int, offset: int = 0, default: bool = ... - ) -> bool: ... # type: ignore[misc] # noqa: E704 + def __new__(cls, *, mask: int, offset: int = 0, default: bool = ...) -> bool: ... # type: ignore[misc] # noqa: E704 def __init__(self, *, mask: int, offset: int = 0, default: object = _MISSING) -> None: self._mask = mask diff --git a/tests/protocol/register_models.py b/tests/protocol/register_models.py index c0a35ba..487c403 100644 --- a/tests/protocol/register_models.py +++ b/tests/protocol/register_models.py @@ -411,7 +411,9 @@ def main() -> None: # pragma: no cover - manual exploration entry point assert float(p.DutyCycle) == 0.5 assert ComplexConfigurationPayload.dtype.itemsize == 17 assert cc.raw_payload.tobytes()[1:4] == b"\x00\x00\x00" - print(f"ComplexConfiguration OK ({ComplexConfigurationPayload.dtype.itemsize} bytes, gap 1..3)") + print( + f"ComplexConfiguration OK ({ComplexConfigurationPayload.dtype.itemsize} bytes, gap 1..3)" + ) ver = VersionPayload( ProtocolVersion=HarpVersion(2, 0, 0), @@ -431,7 +433,9 @@ def main() -> None: # pragma: no cover - manual exploration entry point assert p == HarpVersion(0, 0, 1) print("CustomPayload/RawPayload OK (single-member unwrap)") - p = _roundtrip(CustomMemberConverter, CustomMemberConverterPayload(Header=np.uint8(7), Data=-1234)) + p = _roundtrip( + CustomMemberConverter, CustomMemberConverterPayload(Header=np.uint8(7), Data=-1234) + ) assert int(p.Header) == 7 and int(p.Data) == -1234 print("CustomMemberConverter OK") @@ -452,7 +456,9 @@ def main() -> None: # pragma: no cover - manual exploration entry point assert int(_roundtrip(PulseDO0, np.uint16(9))) == 9 print("PulseDOPort0 / PulseDO0 OK") - p = _roundtrip(StartPulse, StartPulsePayload(DigitalOutput=PwmPort.Pwm1, PulseWidth=np.uint16(300))) + p = _roundtrip( + StartPulse, StartPulsePayload(DigitalOutput=PwmPort.Pwm1, PulseWidth=np.uint16(300)) + ) assert p.DigitalOutput == PwmPort.Pwm1 and int(p.PulseWidth) == 300 print("StartPulse OK") diff --git a/tests/protocol/test_register_modeling.py b/tests/protocol/test_register_modeling.py index 77b2099..9ea2dfb 100644 --- a/tests/protocol/test_register_modeling.py +++ b/tests/protocol/test_register_modeling.py @@ -82,7 +82,9 @@ def test_version_roundtrip(): def test_custom_member_converter_roundtrip(): - p = _roundtrip(CustomMemberConverter, CustomMemberConverterPayload(Header=np.uint8(7), Data=-1234)) + p = _roundtrip( + CustomMemberConverter, CustomMemberConverterPayload(Header=np.uint8(7), Data=-1234) + ) assert int(p.Header) == 7 and int(p.Data) == -1234 @@ -130,7 +132,9 @@ def test_start_pulse_overlapping_masks(): # Two views of one U16 element share storage (one numpy field, itemsize 2). assert StartPulsePayload.dtype.itemsize == 2 assert len(StartPulsePayload.dtype.names) == 1 - p = _roundtrip(StartPulse, StartPulsePayload(DigitalOutput=PwmPort.Pwm1, PulseWidth=np.uint16(300))) + p = _roundtrip( + StartPulse, StartPulsePayload(DigitalOutput=PwmPort.Pwm1, PulseWidth=np.uint16(300)) + ) assert p.DigitalOutput == PwmPort.Pwm1 assert int(p.PulseWidth) == 300 @@ -198,8 +202,11 @@ def test_strict_enum_raises_on_unknown_code(): def test_complex_configuration_to_dataframe(): cc = ComplexConfigurationPayload( - PwmPort=PwmPort.Pwm2, DutyCycle=np.float32(0.5), Frequency=np.float32(1.0), - EventsEnabled=True, Delta=np.uint32(42), + PwmPort=PwmPort.Pwm2, + DutyCycle=np.float32(0.5), + Frequency=np.float32(1.0), + EventsEnabled=True, + Delta=np.uint32(42), ) batch = ComplexConfigurationPayload.from_buffer(cc.raw_payload.tobytes() * 2) df = batch.to_dataframe() From 9425aba3d0799d5d249f9dcb15a0bb3b67d5a317 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Tue, 16 Jun 2026 07:02:28 -0700 Subject: [PATCH 227/267] Fix type errors and examples --- pyproject.toml | 6 ++-- scripts/benchmark_analog_data.py | 10 +++---- scripts/core_registers_example.py | 4 ++- scripts/device_integration.py | 2 +- scripts/heterogenous_register.py | 14 +++++++--- src/harp-protocol/harp/protocol/_message.py | 4 +-- src/harp-protocol/harp/protocol/_payload.py | 28 ++++++++++--------- .../harp/protocol/_payload_converters.py | 2 +- .../harp/protocol/_payload_type.py | 2 +- tests/protocol/test_payload.py | 2 +- 10 files changed, 43 insertions(+), 31 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 02f0a7a..d8417fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,9 @@ extra-paths = ["tests"] [tool.ty.src] include = [ - "harp-protocol/src", - "harp-device/src", + "src/harp-protocol", + "src/harp-device/src", +] +exclude = [ "tests", ] \ No newline at end of file diff --git a/scripts/benchmark_analog_data.py b/scripts/benchmark_analog_data.py index a9e5ec8..f1a4eb8 100644 --- a/scripts/benchmark_analog_data.py +++ b/scripts/benchmark_analog_data.py @@ -43,12 +43,12 @@ ANALOG_COLUMNS = ["analog_input0", "encoder", "analog_input1"] -class AnalogDataPayload(PayloadBase[np.int16]): +class AnalogDataPayload(PayloadBase[np.int16], length=3): """Payload for AnalogData (register 44): three signed 16-bit channels.""" - analog_input0 = Field(converter=IdentityConverter(np.int16)) - encoder = Field(converter=IdentityConverter(np.int16)) - analog_input1 = Field(converter=IdentityConverter(np.int16)) + analog_input0 = Field(converter=IdentityConverter(np.int16), offset=0) + encoder = Field(converter=IdentityConverter(np.int16), offset=1) + analog_input1 = Field(converter=IdentityConverter(np.int16), offset=2) class AnalogData(RegisterBase[AnalogDataPayload]): @@ -63,7 +63,7 @@ class AnalogData(RegisterBase[AnalogDataPayload]): # Helpers # --------------------------------------------------------------------------- -BIN_FILE = REPO_ROOT / "scripts" / "Behavior_44.bin" +BIN_FILE = Path(r"C:\Users\bruno.cruz\Downloads\Behavior_44.bin") def pyharp_read(path: Path, *, include_timestamp: bool = True): diff --git a/scripts/core_registers_example.py b/scripts/core_registers_example.py index b02c4ad..db6275f 100644 --- a/scripts/core_registers_example.py +++ b/scripts/core_registers_example.py @@ -91,7 +91,9 @@ # --------------------------------------------------------------------------- # Bulk read from a .bin file (zero-copy, vectorised) # --------------------------------------------------------------------------- -BIN_FILE = Path(__file__).parent.parent / "notes/Behavior.harp/Behavior_10.bin" +BIN_FILE = Path( + r"C:\git\bruno-f-cruz\analysis-harlow-learning-sets\data\841312_2026-06-15_19-38-50\behavior\Behavior.harp\Behavior_10.bin" +) print(f"\n=== Bulk read from {BIN_FILE.name} ===") _data, timestamps, msg_type, payload = OperationControl.parse_bulk(BIN_FILE.read_bytes()) print(f" {len(timestamps)} frame(s) read") diff --git a/scripts/device_integration.py b/scripts/device_integration.py index 6591426..9bf205e 100644 --- a/scripts/device_integration.py +++ b/scripts/device_integration.py @@ -20,7 +20,7 @@ WhoAmI, ) -PORT = "COM3" +PORT = "COM4" N_READS = 10_000 CORE_REGISTERS = [ diff --git a/scripts/heterogenous_register.py b/scripts/heterogenous_register.py index b1e85fc..9f30849 100644 --- a/scripts/heterogenous_register.py +++ b/scripts/heterogenous_register.py @@ -28,10 +28,12 @@ class FileSettings0Payload(StructPayload[np.uint8]): - cycles: np.uint32 = Field(converter=IdentityConverter(np.uint32)) - duration_us: np.uint32 = Field(converter=IdentityConverter(np.uint32)) - update_frequency_hz: np.uint32 = Field(converter=IdentityConverter(np.uint32)) - path: str = Field(converter=StringConverter(33), default="220khzwaveform") # defaults work too + cycles: np.uint32 = Field(converter=IdentityConverter(np.uint32), offset=0) + duration_us: np.uint32 = Field(converter=IdentityConverter(np.uint32), offset=4) + update_frequency_hz: np.uint32 = Field(converter=IdentityConverter(np.uint32), offset=8) + path: str = Field( + converter=StringConverter(33), default="220khzwaveform", offset=12 + ) # defaults work too # --------------------------------------------------------------------------- @@ -61,6 +63,10 @@ class FileSettings0(RegisterBase[FileSettings0Payload]): print(f"Payload dtype : {FileSettings0Payload.dtype}") print(f"Payload bytes : {payload.raw_payload.tobytes().hex()}") print(f"Payload : {payload}") + print(f" cycles = {payload.cycles}") + print(f" duration_us = {payload.duration_us}") + print(f" update_frequency_hz = {payload.update_frequency_hz}") + print(f" path = {payload.path!r}") # 2. Encode → Harp wire frame frame = FileSettings0.format(payload, message_type=MessageType.Write) diff --git a/src/harp-protocol/harp/protocol/_message.py b/src/harp-protocol/harp/protocol/_message.py index e06e159..ef522a6 100644 --- a/src/harp-protocol/harp/protocol/_message.py +++ b/src/harp-protocol/harp/protocol/_message.py @@ -36,12 +36,12 @@ def __init__( message_type: MessageType, address: int, payload_type: PayloadType, - payload: "bytes" = b"", + payload: bytes = b"", *, port: int = _DEFAULT_PORT, timestamp: float | None = None, ) -> None: - self._bytes: "bytes" = build_message_frame( + self._bytes: bytes = build_message_frame( message_type, address, payload_type, payload, port=port, timestamp=timestamp ) diff --git a/src/harp-protocol/harp/protocol/_payload.py b/src/harp-protocol/harp/protocol/_payload.py index fafd721..e012082 100644 --- a/src/harp-protocol/harp/protocol/_payload.py +++ b/src/harp-protocol/harp/protocol/_payload.py @@ -89,7 +89,7 @@ def __get__(self, obj: "PayloadBase", owner: object = None) -> T: ... def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: if obj is None: return self - return self._converter.decode_scalar(obj._arr[self._name]) + return self._converter.decode_scalar(obj._arr[self._name]) # ty: ignore[invalid-argument-type] def _to_batch(self) -> "_FieldBatch[T]": return _FieldBatch(converter=self._converter) @@ -129,7 +129,7 @@ def _to_batch(self) -> "_BitFlagBatch": return _BitFlagBatch(self._mask, slot=self._slot, dtype=self._dtype) -def _build_enum_lookup(enum_cls: type) -> "tuple[list[str], np.ndarray]": +def _build_enum_lookup(enum_cls: type[enum.IntEnum]) -> "tuple[list[str], np.ndarray]": """Helper for GroupMask to build the category list and code lookup table for a given enum.IntEnum class.""" members = list(enum_cls) categories = [m.name for m in members] @@ -423,7 +423,7 @@ def _validate_no_overlap(cls: type, slots: "dict[str, _FieldSlot]", itemsize: in def _build_struct_dtype( - cls: type, + cls: "type[PayloadBase]", declarations: "list[tuple[str, Field | BitFlag | GroupMask]]", length: int | None, ) -> np.dtype: @@ -466,7 +466,9 @@ def _build_struct_dtype( ) -def _resolve_single_member(cls: type, declarations: "list[tuple[str, Any]]") -> "str | None": +def _resolve_single_member( + cls: "type[PayloadBase]", declarations: "list[tuple[str, Any]]" +) -> "str | None": """A payload with exactly one full-span ``Field`` unwraps to that member on ``parse`` (register-level ``interfaceType``), avoiding a ``.value`` hop.""" if len(declarations) != 1: @@ -541,7 +543,7 @@ def __init__(self, *args: object, **kwargs: object) -> None: for attr_name, value in kwargs.items(): desc = cls._mro_descriptor(attr_name) if isinstance(desc, _FIELD_TYPES): - desc._converter.encode_into(arr[desc._name], value) + desc._converter.encode_into(arr[desc._name], value) # ty: ignore[invalid-argument-type] elif isinstance(desc, _BITFIELD_TYPES): slot = desc._slot mask_in_dtype = np.array(desc._mask, dtype=desc._dtype) @@ -549,7 +551,7 @@ def __init__(self, *args: object, **kwargs: object) -> None: if value: arr[slot] |= mask_in_dtype else: - int_val = desc._encode_value(value) + int_val = desc._encode_value(value) # ty: ignore[unresolved-attribute] shifted = np.array((int_val << desc._shift) & desc._mask, dtype=desc._dtype) arr[slot] = (arr[slot] & ~mask_in_dtype) | shifted elif attr_name in names: @@ -682,17 +684,17 @@ def to_dataframe(self, *, decode_enums: bool = True) -> pd.DataFrame: for f in repr_fields: desc = cls._mro_descriptor(f) if isinstance(desc, _GROUP_MASK_TYPES): - slot_col = arr[desc._slot] + slot_col = arr[desc._slot] # ty: ignore[invalid-argument-type] raw = (slot_col & desc._mask) >> desc._shift if desc._enum is not None and decode_enums: - codes = desc._code_lookup[raw] + codes = desc._code_lookup[raw] # ty: ignore[not-subscriptable] cols[f] = pd.Categorical.from_codes(codes, categories=desc._categories) elif desc._enum is None and desc._converter is not None: cols[f] = desc._converter.decode_batch(raw.astype(desc._converter.dtype)) else: cols[f] = raw elif isinstance(desc, _BIT_FLAG_TYPES): - slot_col = arr[desc._slot] + slot_col = arr[desc._slot] # ty: ignore[invalid-argument-type] cols[f] = (slot_col & desc._mask) != 0 else: cols[f] = np.atleast_1d(getattr(self, f)) @@ -701,7 +703,7 @@ def to_dataframe(self, *, decode_enums: bool = True) -> pd.DataFrame: cols = {} names = self.dtype.names single_value_slot = names == ("value",) - for name in names: + for name in names: # ty: ignore[not-iterable] desc = cls._mro_descriptor(name) uses_converter = isinstance(desc, _FIELD_TYPES) and not isinstance( desc._converter, _IdentityConverter @@ -710,8 +712,8 @@ def to_dataframe(self, *, decode_enums: bool = True) -> pd.DataFrame: cols[name] = np.atleast_1d(getattr(self, name)) continue - field_dtype, _ = self.dtype.fields[name] - sub = arr[name] + field_dtype, _ = self.dtype.fields[name] # ty: ignore[invalid-assignment, invalid-argument-type, not-subscriptable] + sub = arr[name] # ty: ignore[invalid-argument-type] if field_dtype.subdtype is None: cols[name] = sub else: @@ -825,7 +827,7 @@ def __init__(self, value: object = _MISSING_INIT, /, **kwargs: object) -> None: raise TypeError(f"{type(self).__name__}() requires a value") if kwargs: raise TypeError(f"{type(self).__name__}() got unexpected kwargs: {sorted(kwargs)}") - self._arr = np.asarray(value, dtype=self.dtype) + self._arr = np.asarray(value, dtype=self.dtype) # ty: ignore[invalid-assignment] @classmethod def unwrap(cls, arr: "np.ndarray") -> Any: diff --git a/src/harp-protocol/harp/protocol/_payload_converters.py b/src/harp-protocol/harp/protocol/_payload_converters.py index c5c16fd..08f42e7 100644 --- a/src/harp-protocol/harp/protocol/_payload_converters.py +++ b/src/harp-protocol/harp/protocol/_payload_converters.py @@ -203,7 +203,7 @@ def __init__(self, length: int, encoding: str = "ascii") -> None: self.dtype = np.dtype((np.uint8, (length,))) def decode_scalar(self, view: np.generic) -> str: - return bytes(view).rstrip(b"\x00").decode(self._encoding) + return bytes(view).rstrip(b"\x00").decode(self._encoding) # ty: ignore[invalid-argument-type] def decode_batch(self, view: NDArray[np.generic]) -> Any: return np.array( diff --git a/src/harp-protocol/harp/protocol/_payload_type.py b/src/harp-protocol/harp/protocol/_payload_type.py index 7d37a3c..a955416 100644 --- a/src/harp-protocol/harp/protocol/_payload_type.py +++ b/src/harp-protocol/harp/protocol/_payload_type.py @@ -21,7 +21,7 @@ class PayloadType(Enum): @property def numpy_dtype(self) -> np.dtype: - return self.value # type: ignore + return self.value @dataclass(frozen=True) diff --git a/tests/protocol/test_payload.py b/tests/protocol/test_payload.py index d5f8c57..8a7fad1 100644 --- a/tests/protocol/test_payload.py +++ b/tests/protocol/test_payload.py @@ -12,7 +12,7 @@ class SimplePayload(PayloadBase): class BitPackedPayload(PayloadBase): packed = Field(converter=_IdentityConverter("u1")) - def to_dataframe(self) -> pd.DataFrame: + def to_dataframe(self, *, decode_enums: bool = True) -> pd.DataFrame: return pd.DataFrame( { "flag_a": (self.raw_payload["packed"] & 0x01).astype(bool), From 653e98ff29b89dabc6c78deaaeb90e4023a0d3cf Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Tue, 23 Jun 2026 08:41:33 -0700 Subject: [PATCH 228/267] Simplify repr field collection --- src/harp-protocol/harp/protocol/_payload.py | 27 ++++++++++++--------- tests/protocol/register_models.py | 1 - tests/protocol/test_register.py | 24 ++++++++++++++++++ 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/harp-protocol/harp/protocol/_payload.py b/src/harp-protocol/harp/protocol/_payload.py index e012082..7dee5d3 100644 --- a/src/harp-protocol/harp/protocol/_payload.py +++ b/src/harp-protocol/harp/protocol/_payload.py @@ -588,6 +588,21 @@ def _collect_defaults(cls) -> "dict[str, Any]": out[attr] = val._default return out + @classmethod + def _collect_repr_fields(cls) -> "tuple[str, ...]": + """Declared attribute names in MRO + definition order. + + Covers plain ``Field``s and masked sub-fields alike (a single dtype slot + may back several bitfields, so this enumerates declarations rather than + ``dtype.names``). + """ + names: list[str] = [] + for klass in reversed(cls.__mro__): + for attr, val in klass.__dict__.items(): + if isinstance(val, _SCALAR_DECLARATION_TYPES) and attr not in names: + names.append(attr) + return tuple(names) + def __init_subclass__( cls, *, @@ -627,17 +642,7 @@ def __init_subclass__( cls._single_member = _resolve_single_member(cls, own_declarations) if "_repr_fields" not in cls.__dict__: - bitfield_names = tuple( - name for name, val in vars(cls).items() if isinstance(val, (BitFlag, GroupMask)) - ) - if bitfield_names: - cls._repr_fields = bitfield_names - else: - names = cls.dtype.names if hasattr(cls, "dtype") else None - if names is not None and names != ("value",): - cls._repr_fields = names - else: - cls._repr_fields = ("value",) + cls._repr_fields = cls._collect_repr_fields() cls._scalar_cls = cls cls._batch_cls = cls # rebound below once Batch is generated diff --git a/tests/protocol/register_models.py b/tests/protocol/register_models.py index 487c403..2840563 100644 --- a/tests/protocol/register_models.py +++ b/tests/protocol/register_models.py @@ -192,7 +192,6 @@ class ComplexConfigurationPayload(StructPayload[np.uint8], length=17): Frequency: np.float32 = Field(IdentityConverter(np.float32), offset=8) EventsEnabled: bool = Field(BoolConverter(), offset=12) Delta: np.uint32 = Field(IdentityConverter(np.uint32), offset=13) - _repr_fields = ("PwmPort", "DutyCycle", "Frequency", "EventsEnabled", "Delta") class ComplexConfiguration(RegisterBase[ComplexConfigurationPayload]): diff --git a/tests/protocol/test_register.py b/tests/protocol/test_register.py index 9587ee3..cfc24a1 100644 --- a/tests/protocol/test_register.py +++ b/tests/protocol/test_register.py @@ -18,6 +18,8 @@ PayloadU32, PayloadU64, Field, + BitFlag, + GroupMask, _IdentityConverter, ) from harp.protocol._payload_type import PayloadType @@ -430,3 +432,25 @@ class P(PayloadBase): beta = Field(converter=_IdentityConverter(" Date: Tue, 23 Jun 2026 12:10:22 -0700 Subject: [PATCH 229/267] Allow fields to also define a masked value --- src/harp-protocol/harp/protocol/_payload.py | 220 ++++++++++++-------- tests/protocol/register_models.py | 18 +- tests/protocol/test_register.py | 9 +- 3 files changed, 145 insertions(+), 102 deletions(-) diff --git a/src/harp-protocol/harp/protocol/_payload.py b/src/harp-protocol/harp/protocol/_payload.py index 7dee5d3..f22443d 100644 --- a/src/harp-protocol/harp/protocol/_payload.py +++ b/src/harp-protocol/harp/protocol/_payload.py @@ -50,13 +50,22 @@ def _mask_trailing_zeros(mask: int) -> int: class Field(Generic[T]): - """Descriptor for a whole-element (or reinterpreted multi-element) payload view. - - The view reads ``converter.dtype.itemsize`` bytes starting at ``offset`` (in - base-element units; see :class:`StructPayload`) and runs them through - ``converter``. The converter owns its own ``dtype`` (byte layout) and is - independent of the payload's base element type, so the same converter works - under any register width. + """Descriptor for a payload view decoded through a :class:`Converter`. + + Two modes, selected by ``mask``: + + * **Whole-element** (``mask=None``, the default) — the view reads + ``converter.dtype.itemsize`` bytes starting at ``offset`` (in base-element + units; see :class:`StructPayload`) and runs them through ``converter``. The + converter owns its own ``dtype`` (byte layout) and is independent of the + payload's base element type, so the same converter works under any register + width. + * **Masked sub-field** (``mask`` set) — the raw value is extracted as + ``(element & mask) >> shift`` from the payload's *base element* at ``offset`` + and then run through ``converter`` (which dictates the output type). The + right-shift is derived from ``mask`` (its trailing-zero count). Several masked + fields at the same offset share the element slot automatically, and may share + it with a :class:`GroupMask` or :class:`BitFlag` on the same word. ``offset`` defaults to ``0``. Omitting it suits a payload with a single member; when a payload has several distinct slots, each must declare an @@ -68,20 +77,42 @@ class Field(Generic[T]): # type-mismatch error. At runtime __new__ is not defined and a Field instance is # returned as normal. def __new__( # type: ignore[misc] - cls, converter: "_Converter[T]", *, offset: int = 0, default: "T" = ... + cls, + converter: "_Converter[T]", + *, + mask: int | None = None, + offset: int = 0, + default: "T" = ..., ) -> "T": ... def __init__( - self, converter: _Converter[T], *, offset: int = 0, default: object = _MISSING + self, + converter: _Converter[T], + *, + mask: int | None = None, + offset: int = 0, + default: object = _MISSING, ) -> None: self._converter = converter self._name: str | None = None + self._mask = mask + self._shift = _mask_trailing_zeros(mask) if mask is not None else 0 self._offset = offset self._default = default + # Derived in PayloadBase.__init_subclass__ for the masked variant: + self._slot: str = "value" + self._dtype: np.dtype = _DEFAULT_ELEMENT def __set_name__(self, owner: object, name: str) -> None: self._name = name + def _encode_value(self, value: Any) -> int: + """Map a user value back to the integer to be masked + shifted into the slot + (masked variant only).""" + tmp = np.zeros((), dtype=self._converter.dtype) + self._converter.encode_into(tmp, value) + return int(tmp) + @overload def __get__(self, obj: None, owner: object = None) -> "Field[T]": ... @overload @@ -89,10 +120,18 @@ def __get__(self, obj: "PayloadBase", owner: object = None) -> T: ... def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: if obj is None: return self + if self._mask is not None: + raw = (obj._arr[self._slot] & self._mask) >> self._shift + return self._converter.decode_scalar(self._converter.dtype.type(raw)) return self._converter.decode_scalar(obj._arr[self._name]) # ty: ignore[invalid-argument-type] def _to_batch(self) -> "_FieldBatch[T]": - return _FieldBatch(converter=self._converter) + return _FieldBatch( + converter=self._converter, + mask=self._mask, + slot=self._slot, + dtype=self._dtype, + ) class BitFlag: @@ -142,14 +181,12 @@ def _build_enum_lookup(enum_cls: type[enum.IntEnum]) -> "tuple[list[str], np.nda class GroupMask(Generic[E]): - """Descriptor for a masked, shifted sub-field of a payload element. + """Descriptor for a masked, shifted enum sub-field of a payload element. - The raw value is extracted as ``(element & mask) >> shift`` and then mapped: - - * ``enum=`` — to an ``enum.IntEnum`` member (strict; unknown code raises); - * ``converter=`` — through a :class:`Converter` applied to the *masked* - integer (numeric casts, bool, custom); - * neither — returned as the raw masked numpy integer (the element's dtype). + Readable sugar over a masked :class:`Field`: the raw value is extracted as + ``(element & mask) >> shift`` and mapped strictly to an ``enum.IntEnum`` member + (an unknown code raises). ``enum=`` is required; for masked *numeric* fields use + ``Field(converter=..., mask=...)`` instead. The right-shift is always derived from ``mask`` (its trailing-zero count, so the field aligns to bit 0); ``offset`` defaults to ``0``. The element width and @@ -159,61 +196,39 @@ class GroupMask(Generic[E]): if TYPE_CHECKING: # enum variant -> the field type is the enum - @overload def __new__( # type: ignore[misc] # noqa: E704 cls, *, mask: int, enum: "type[E]", offset: int = 0, default: "E" = ... ) -> "E": ... - # converter variant -> the field type is the converter's output type - @overload - def __new__( # type: ignore[misc] # noqa: E704 - cls, *, mask: int, converter: "_Converter[T]", offset: int = 0, default: "T" = ... - ) -> "T": ... - # raw variant -> a plain integer - @overload - def __new__( # type: ignore[misc] # noqa: E704 - cls, *, mask: int, offset: int = 0, default: int = ... - ) -> int: ... - def __new__(cls, **kwargs: Any) -> Any: ... # type: ignore[misc] # noqa: E704 def __init__( self, *, mask: int, - enum: type[E] | None = None, - converter: "_Converter[Any] | None" = None, + enum: type[E], offset: int = 0, default: object = _MISSING, ) -> None: - if enum is not None and converter is not None: - raise TypeError("GroupMask accepts at most one of 'enum' or 'converter'") + if enum is None: + raise TypeError( + "GroupMask requires 'enum'; use Field(converter=..., mask=...) for " + "masked numeric sub-fields" + ) self._mask = mask self._shift = _mask_trailing_zeros(mask) self._enum = enum - self._converter = converter self._offset = offset self._default = default # Derived in PayloadBase.__init_subclass__: self._slot: str = "value" self._dtype: np.dtype = _DEFAULT_ELEMENT - if enum is not None: - self._categories, self._code_lookup = _build_enum_lookup(enum) - else: - self._categories, self._code_lookup = None, None + self._categories, self._code_lookup = _build_enum_lookup(enum) def _decode_raw(self, raw: Any) -> Any: - """Map an extracted (already masked + shifted) integer to its value.""" - if self._enum is not None: - return self._enum(int(raw)) - if self._converter is not None: - return self._converter.decode_scalar(self._converter.dtype.type(raw)) - return raw + """Map an extracted (already masked + shifted) integer to its enum member.""" + return self._enum(int(raw)) def _encode_value(self, value: Any) -> int: """Map a user value back to the integer to be masked + shifted into the slot.""" - if self._converter is not None and self._enum is None: - tmp = np.zeros((), dtype=self._converter.dtype) - self._converter.encode_into(tmp, value) - return int(tmp) return int(value) @overload @@ -230,7 +245,6 @@ def _to_batch(self) -> "_GroupMaskBatch[E]": return _GroupMaskBatch( self._mask, self._enum, - converter=self._converter, slot=self._slot, dtype=self._dtype, ) @@ -245,9 +259,20 @@ def _to_batch(self) -> "_GroupMaskBatch[E]": class _FieldBatch(Generic[T]): """Same as _Field but returns an NDArray view for batch payloads rather than a scalar value.""" - def __init__(self, *, converter: _Converter[T]) -> None: + def __init__( + self, + *, + converter: _Converter[T], + mask: int | None = None, + slot: str = "value", + dtype: "np.dtype | str | type" = np.uint8, + ) -> None: self._converter = converter self._name: str | None = None + self._mask = mask + self._shift = _mask_trailing_zeros(mask) if mask is not None else 0 + self._slot = slot + self._dtype = np.dtype(dtype) def __set_name__(self, owner: object, name: str) -> None: self._name = name @@ -259,6 +284,9 @@ def __get__(self, obj: "PayloadBase", owner: object = None) -> "NDArray[Any]": . def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: if obj is None: return self + if self._mask is not None: + raw = (obj._arr[self._slot] & self._mask) >> self._shift + return self._converter.decode_batch(raw.astype(self._converter.dtype)) return self._converter.decode_batch(obj._arr[self._name]) @@ -292,22 +320,17 @@ class _GroupMaskBatch(Generic[E]): def __init__( self, mask: int, - enum: type[E] | None, + enum: type[E], *, - converter: "_Converter[Any] | None" = None, slot: str = "value", dtype: "np.dtype | str | type" = np.uint8, ) -> None: self._mask = mask self._shift = _mask_trailing_zeros(mask) self._enum = enum - self._converter = converter self._slot = slot self._dtype = np.dtype(dtype) - if enum is not None: - self._categories, self._code_lookup = _build_enum_lookup(enum) - else: - self._categories, self._code_lookup = None, None + self._categories, self._code_lookup = _build_enum_lookup(enum) @overload def __get__(self, obj: None, owner: object = None) -> "_GroupMaskBatch[E]": ... @@ -316,12 +339,8 @@ def __get__(self, obj: "PayloadBase", owner: object = None) -> "NDArray[Any]": . def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: if obj is None: return self - raw = (obj._arr[self._slot] & self._mask) >> self._shift - # Enum batches return raw integer codes (see to_dataframe for Categorical - # decoding). Converter batches decode element-wise; raw stays integer. - if self._enum is None and self._converter is not None: - return self._converter.decode_batch(raw.astype(self._converter.dtype)) - return raw + # Enum batches return raw integer codes (see to_dataframe for Categorical decoding). + return (obj._arr[self._slot] & self._mask) >> self._shift _PT = TypeVar("_PT", bound="PayloadBase[Any]") @@ -361,6 +380,17 @@ def __getattr__(self, name: str) -> "NDArray[Any]": ... # type: ignore[empty-bo _GROUP_MASK_TYPES = (GroupMask, _GroupMaskBatch) _BIT_FLAG_TYPES = (BitFlag, _BitFlagBatch) + +def _is_masked(desc: object) -> bool: + """A descriptor that reads a masked + shifted sub-field of a shared element slot: + every ``BitFlag``/``GroupMask`` (always masked), or a ``Field`` declaring ``mask=``. + Unmasked ``Field`` (whole-element view, own slot) returns False.""" + if isinstance(desc, _BITFIELD_TYPES): + return True + if isinstance(desc, _FIELD_TYPES): + return desc._mask is not None + return False + # value/raw_payload deliberately omitted: overriding them is the intended # pattern for single-slot converter-driven payloads. _RESERVED_FIELD_NAMES = frozenset({"_arr", "_dtype", "_repr_fields", "Batch"}) @@ -435,9 +465,20 @@ def _build_struct_dtype( for attr_name, val in declarations: byte_offset = val._offset * elem_size - if isinstance(val, (BitFlag, GroupMask)): + # A plain Field (no mask=) is the only whole-element view; everything else + # (BitFlag, GroupMask, or a Field with mask=) is a masked sub-field. The + # isinstance form — rather than _is_masked() — is what lets the type checker + # narrow `val` for the slot/converter attribute access in each branch. + if isinstance(val, Field) and val._mask is None: # whole-element Field — own slot + field_dtype = val._converter.dtype + if attr_name in slots: + raise TypeError(f"{cls.__name__}: duplicate field name {attr_name!r}") + slots[attr_name] = _FieldSlot(field_dtype, byte_offset) + else: # masked sub-field — share the base-element slot + mask = val._mask + assert mask is not None # invariant for every masked descriptor val._dtype = elem - _validate_mask_fits(cls, attr_name, val._mask, elem) + _validate_mask_fits(cls, attr_name, mask, elem) shared_slot = mask_slot_by_byte_offset.get(byte_offset) if shared_slot is not None: val._slot = shared_slot @@ -445,11 +486,6 @@ def _build_struct_dtype( mask_slot_by_byte_offset[byte_offset] = attr_name val._slot = attr_name slots[attr_name] = _FieldSlot(elem, byte_offset) - else: # Field - field_dtype = val._converter.dtype - if attr_name in slots: - raise TypeError(f"{cls.__name__}: duplicate field name {attr_name!r}") - slots[attr_name] = _FieldSlot(field_dtype, byte_offset) if length is not None: itemsize = length * elem_size @@ -474,7 +510,11 @@ def _resolve_single_member( if len(declarations) != 1: return None attr, val = declarations[0] - if isinstance(val, Field) and val._converter.dtype.itemsize == cls.dtype.itemsize: # type: ignore[attr-defined] + if ( + isinstance(val, Field) + and val._mask is None + and val._converter.dtype.itemsize == cls.dtype.itemsize # type: ignore[attr-defined] + ): return attr return None @@ -542,18 +582,20 @@ def __init__(self, *args: object, **kwargs: object) -> None: # collides with the first masked field's attribute name. for attr_name, value in kwargs.items(): desc = cls._mro_descriptor(attr_name) - if isinstance(desc, _FIELD_TYPES): - desc._converter.encode_into(arr[desc._name], value) # ty: ignore[invalid-argument-type] - elif isinstance(desc, _BITFIELD_TYPES): - slot = desc._slot + if isinstance(desc, BitFlag): # single bit -> set/clear mask_in_dtype = np.array(desc._mask, dtype=desc._dtype) - if isinstance(desc, _BIT_FLAG_TYPES): - if value: - arr[slot] |= mask_in_dtype - else: - int_val = desc._encode_value(value) # ty: ignore[unresolved-attribute] - shifted = np.array((int_val << desc._shift) & desc._mask, dtype=desc._dtype) - arr[slot] = (arr[slot] & ~mask_in_dtype) | shifted + if value: + arr[desc._slot] |= mask_in_dtype + elif isinstance(desc, Field) and desc._mask is None: # whole-element Field + desc._converter.encode_into(arr[desc._name], value) # ty: ignore[invalid-argument-type] + elif isinstance(desc, (GroupMask, Field)): + # masked sub-field (enum or numeric) -> encode, shift, merge into shared slot + mask = desc._mask + assert mask is not None # invariant for masked descriptors + mask_in_dtype = np.array(mask, dtype=desc._dtype) + int_val = desc._encode_value(value) + shifted = np.array((int_val << desc._shift) & mask, dtype=desc._dtype) + arr[desc._slot] = (arr[desc._slot] & ~mask_in_dtype) | shifted elif attr_name in names: arr[attr_name] = value # raw slot with no descriptor else: @@ -683,21 +725,23 @@ def to_dataframe(self, *, decode_enums: bool = True) -> pd.DataFrame: cls = type(self) repr_fields = self._repr_fields - has_bitfield = any(isinstance(cls._mro_descriptor(f), _BITFIELD_TYPES) for f in repr_fields) + has_bitfield = any(_is_masked(cls._mro_descriptor(f)) for f in repr_fields) if has_bitfield: cols: dict[str, object] = {} for f in repr_fields: desc = cls._mro_descriptor(f) - if isinstance(desc, _GROUP_MASK_TYPES): + if isinstance(desc, _GROUP_MASK_TYPES): # masked enum sub-field slot_col = arr[desc._slot] # ty: ignore[invalid-argument-type] raw = (slot_col & desc._mask) >> desc._shift - if desc._enum is not None and decode_enums: - codes = desc._code_lookup[raw] # ty: ignore[not-subscriptable] + if decode_enums: + codes = desc._code_lookup[raw] cols[f] = pd.Categorical.from_codes(codes, categories=desc._categories) - elif desc._enum is None and desc._converter is not None: - cols[f] = desc._converter.decode_batch(raw.astype(desc._converter.dtype)) else: cols[f] = raw + elif isinstance(desc, _FIELD_TYPES) and desc._mask is not None: # masked numeric sub-field + slot_col = arr[desc._slot] # ty: ignore[invalid-argument-type] + raw = (slot_col & desc._mask) >> desc._shift + cols[f] = desc._converter.decode_batch(raw.astype(desc._converter.dtype)) elif isinstance(desc, _BIT_FLAG_TYPES): slot_col = arr[desc._slot] # ty: ignore[invalid-argument-type] cols[f] = (slot_col & desc._mask) != 0 diff --git a/tests/protocol/register_models.py b/tests/protocol/register_models.py index 2840563..3ccfab2 100644 --- a/tests/protocol/register_models.py +++ b/tests/protocol/register_models.py @@ -16,8 +16,8 @@ * ``Converter`` instances operate on their own byte layout and are independent of the register element type (custom codecs read raw ``uint8`` sub-arrays), so the same ``HarpVersionConverter`` works under a U8 or a U32 register. -* Masked sub-fields use ``GroupMask`` (enum / converter / raw); the right-shift is - derived from the mask's trailing zeros. +* Masked sub-fields use ``GroupMask`` (enum) or ``Field(converter=..., mask=...)`` + (numeric); the right-shift is derived from the mask's trailing zeros. * The register ``length`` (base elements) fixes ``itemsize`` so byte gaps survive. * Enum decoding is strict (an out-of-range code raises) — a deliberate divergence from the C# generator's unchecked cast. @@ -269,8 +269,8 @@ class CustomMemberConverter(RegisterBase[CustomMemberConverterPayload]): class BitmaskSplitterPayload(StructPayload[np.uint8]): - Low: np.int32 = GroupMask(mask=0x0F, converter=IdentityConverter(np.int32)) - High: np.int32 = GroupMask(mask=0xF0, converter=IdentityConverter(np.int32)) + Low: np.int32 = Field(IdentityConverter(np.int32), mask=0x0F) + High: np.int32 = Field(IdentityConverter(np.int32), mask=0xF0) class BitmaskSplitter(RegisterBase[BitmaskSplitterPayload]): @@ -327,7 +327,7 @@ class PulseDO0(RegisterU16): class StartPulsePayload(StructPayload[np.uint16]): DigitalOutput: PwmPort = GroupMask(enum=PwmPort, mask=0xC00) - PulseWidth: np.uint16 = GroupMask(mask=0x3FF) + PulseWidth: np.uint16 = Field(IdentityConverter(np.uint16), mask=0x3FF) class StartPulse(RegisterBase[StartPulsePayload]): @@ -343,11 +343,11 @@ class StartPulse(RegisterBase[StartPulsePayload]): class StartPulseTrainPayload(StructPayload[np.uint16], length=2): DigitalOutput: PwmPort = GroupMask(enum=PwmPort, mask=0xC00, offset=0) - PulseWidth: np.uint16 = GroupMask(mask=0x3FF, offset=0) - Frequency: np.uint8 = GroupMask( - mask=0xFF00, converter=IdentityConverter(np.uint8), offset=1, default=1 + PulseWidth: np.uint16 = Field(IdentityConverter(np.uint16), mask=0x3FF, offset=0) + Frequency: np.uint8 = Field( + IdentityConverter(np.uint8), mask=0xFF00, offset=1, default=np.uint8(1) ) - PulseCount: np.uint8 = GroupMask(mask=0xFF, converter=IdentityConverter(np.uint8), offset=1) + PulseCount: np.uint8 = Field(IdentityConverter(np.uint8), mask=0xFF, offset=1) class StartPulseTrain(RegisterBase[StartPulseTrainPayload]): diff --git a/tests/protocol/test_register.py b/tests/protocol/test_register.py index cfc24a1..4854f71 100644 --- a/tests/protocol/test_register.py +++ b/tests/protocol/test_register.py @@ -19,7 +19,6 @@ PayloadU64, Field, BitFlag, - GroupMask, _IdentityConverter, ) from harp.protocol._payload_type import PayloadType @@ -439,7 +438,7 @@ def test_repr_fields_auto_derived_mixed_bitfield_and_field(): in declaration order (the masked slot must not shadow the plain fields).""" class P(PayloadBase): - flags = GroupMask(mask=0xFF, offset=0) + flags = Field(converter=_IdentityConverter("u1"), mask=0xFF, offset=0) scale = Field(converter=_IdentityConverter(" Date: Wed, 24 Jun 2026 13:12:57 -0700 Subject: [PATCH 230/267] Deprecate converter_registry --- .../harp/protocol/_payload_converters.py | 44 ------------------- 1 file changed, 44 deletions(-) diff --git a/src/harp-protocol/harp/protocol/_payload_converters.py b/src/harp-protocol/harp/protocol/_payload_converters.py index 08f42e7..9255c7f 100644 --- a/src/harp-protocol/harp/protocol/_payload_converters.py +++ b/src/harp-protocol/harp/protocol/_payload_converters.py @@ -1,6 +1,5 @@ import enum as _enum from abc import ABC, abstractmethod -from collections.abc import Callable from typing import Any, Generic, TypeVar, cast import numpy as np @@ -9,37 +8,6 @@ T = TypeVar("T") NpScalarT = TypeVar("NpScalarT", bound=np.generic) E = TypeVar("E", bound=_enum.IntEnum) -_ConverterClsT = TypeVar("_ConverterClsT", bound="type[Converter[Any]]") - -# --------------------------------------------------------------------------- -# Registry -# --------------------------------------------------------------------------- - -converter_registry: "dict[str, type[Converter[Any]]]" = {} - - -def register_converter(*, name: str) -> "Callable[[_ConverterClsT], _ConverterClsT]": - """Class decorator that registers a ``Converter`` subclass under ``name``. - - Raises ``ValueError`` if the name is already registered. - - Usage:: - - @register_converter(name="my_converter") - class MyConverter(Converter[int]): ... - """ - - def _register(cls: "_ConverterClsT") -> "_ConverterClsT": - if name in converter_registry: - raise ValueError( - f"A converter named {name!r} is already registered " - f"(existing: {converter_registry[name]!r}, new: {cls!r})" - ) - converter_registry[name] = cls - return cls - - return _register - # --------------------------------------------------------------------------- # Base class @@ -93,55 +61,46 @@ def encode_into(self, view: NDArray[np.generic], value: NpScalarT) -> None: view[...] = value -@register_converter(name="byte") class UInt8Converter(IdentityConverter[np.uint8]): def __init__(self) -> None: super().__init__(np.uint8) -@register_converter(name="sbyte") class SInt8Converter(IdentityConverter[np.int8]): def __init__(self) -> None: super().__init__(np.int8) -@register_converter(name="ushort") class UInt16Converter(IdentityConverter[np.uint16]): def __init__(self) -> None: super().__init__(np.uint16) -@register_converter(name="short") class Int16Converter(IdentityConverter[np.int16]): def __init__(self) -> None: super().__init__(np.int16) -@register_converter(name="uint") class UInt32Converter(IdentityConverter[np.uint32]): def __init__(self) -> None: super().__init__(np.uint32) -@register_converter(name="int") class Int32Converter(IdentityConverter[np.int32]): def __init__(self) -> None: super().__init__(np.int32) -@register_converter(name="ulong") class UInt64Converter(IdentityConverter[np.uint64]): def __init__(self) -> None: super().__init__(np.uint64) -@register_converter(name="long") class Int64Converter(IdentityConverter[np.int64]): def __init__(self) -> None: super().__init__(np.int64) -@register_converter(name="float") class FloatConverter(IdentityConverter[np.float32]): def __init__(self) -> None: super().__init__(np.float32) @@ -191,12 +150,9 @@ def encode_into(self, view: NDArray[np.generic], value: E) -> None: view[...] = int(value) -@register_converter(name="string") # TODO check this name against Bonsai class StringConverter(Converter[str]): """Converts a fixed-length byte array to/from a Python ``str``.""" - init_kwarg_type = str - def __init__(self, length: int, encoding: str = "ascii") -> None: self._length = length self._encoding = encoding From 3db751843694a05041f3ad042aeb865cf078e021 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Wed, 24 Jun 2026 13:13:11 -0700 Subject: [PATCH 231/267] Deprecate converter_registry --- src/harp-protocol/harp/protocol/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/harp-protocol/harp/protocol/__init__.py b/src/harp-protocol/harp/protocol/__init__.py index e4507d0..78a22ea 100644 --- a/src/harp-protocol/harp/protocol/__init__.py +++ b/src/harp-protocol/harp/protocol/__init__.py @@ -6,7 +6,6 @@ EnumConverter, IdentityConverter, StringConverter, - register_converter, ) from ._payload import ( PayloadBase, @@ -71,7 +70,6 @@ "StringConverter", "BoolConverter", "EnumConverter", - "register_converter", # Payload DSL "PayloadBase", "StructPayload", From 1d2e8e54c9bd0a1bd5f16f57b93b678f40fd53ca Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Wed, 24 Jun 2026 13:35:41 -0700 Subject: [PATCH 232/267] Add support for converters in AnonymousPayload --- scripts/core_registers_example.py | 3 +- src/harp-device/src/harp/device/_registers.py | 10 +--- src/harp-protocol/harp/protocol/_payload.py | 43 ++++++++++++- src/harp-protocol/harp/protocol/_register.py | 5 +- tests/protocol/test_register.py | 60 ++++++++++++++++++- 5 files changed, 108 insertions(+), 13 deletions(-) diff --git a/scripts/core_registers_example.py b/scripts/core_registers_example.py index db6275f..4f2eb91 100644 --- a/scripts/core_registers_example.py +++ b/scripts/core_registers_example.py @@ -14,7 +14,6 @@ OperationMode, # Payload classes ClockConfigPayload, - DeviceNamePayload, OperationControlPayload, ResetDevicePayload, # Registers @@ -54,7 +53,7 @@ ), ), (ResetDevice, ResetDevicePayload(restore_default=True, restore_name=True)), - (DeviceName, DeviceNamePayload(value="my-harp-device")), + (DeviceName, "my-harp-device"), (ClockConfig, ClockConfigPayload(clock_repeater=True, clock_unlock=True)), (Heartbeat, np.uint16(1)), (HwVersionH, np.uint8(2)), diff --git a/src/harp-device/src/harp/device/_registers.py b/src/harp-device/src/harp/device/_registers.py index 4c18f79..0b86ee4 100644 --- a/src/harp-device/src/harp/device/_registers.py +++ b/src/harp-device/src/harp/device/_registers.py @@ -2,7 +2,7 @@ from typing import ClassVar import numpy as np -from harp.protocol._payload import StructPayload, BitFlag, Field, GroupMask +from harp.protocol._payload import AnonymousPayload, BitFlag, GroupMask, StructPayload from harp.protocol._payload_converters import StringConverter as _StringConverter from harp.protocol._payload_type import PayloadType from harp.protocol._register import RegisterBase, RegisterU8, RegisterU16, RegisterU32 @@ -53,16 +53,12 @@ class ResetDevicePayload(StructPayload[np.uint8]): boot_from_eeprom: bool = BitFlag(mask=0x80, default=False) -class DeviceNamePayload(StructPayload[np.uint8]): +class DeviceNamePayload(AnonymousPayload, converter=_StringConverter(25)): """Payload for the DeviceName register (address 12). Stores a user-specified ASCII device name padded to 25 bytes. """ - _MAX_LEN: ClassVar[int] = 25 - - value: str = Field(converter=_StringConverter(_MAX_LEN)) - class ClockConfigPayload(StructPayload[np.uint8]): """Payload for the ClockConfiguration register (address 14).""" @@ -104,7 +100,7 @@ class ResetDevice(RegisterBase[ResetDevicePayload]): payload_class = ResetDevicePayload -class DeviceName(RegisterBase[DeviceNamePayload]): +class DeviceName(RegisterBase[str]): address: ClassVar[int] = 12 payload_type: ClassVar[PayloadType] = PayloadType.U8 payload_class = DeviceNamePayload diff --git a/src/harp-protocol/harp/protocol/_payload.py b/src/harp-protocol/harp/protocol/_payload.py index f22443d..cf957f9 100644 --- a/src/harp-protocol/harp/protocol/_payload.py +++ b/src/harp-protocol/harp/protocol/_payload.py @@ -391,6 +391,7 @@ def _is_masked(desc: object) -> bool: return desc._mask is not None return False + # value/raw_payload deliberately omitted: overriding them is the intended # pattern for single-slot converter-driven payloads. _RESERVED_FIELD_NAMES = frozenset({"_arr", "_dtype", "_repr_fields", "Batch"}) @@ -738,7 +739,9 @@ def to_dataframe(self, *, decode_enums: bool = True) -> pd.DataFrame: cols[f] = pd.Categorical.from_codes(codes, categories=desc._categories) else: cols[f] = raw - elif isinstance(desc, _FIELD_TYPES) and desc._mask is not None: # masked numeric sub-field + elif ( + isinstance(desc, _FIELD_TYPES) and desc._mask is not None + ): # masked numeric sub-field slot_col = arr[desc._slot] # ty: ignore[invalid-argument-type] raw = (slot_col & desc._mask) >> desc._shift cols[f] = desc._converter.decode_batch(raw.astype(desc._converter.dtype)) @@ -855,14 +858,34 @@ class PayloadU16(AnonymousPayload, scalar_dtype=" None: + if converter is not None: + cls._converter = converter + if scalar_dtype is None: + scalar_dtype = converter.dtype if scalar_dtype is not None: cls.dtype = np.dtype(scalar_dtype) cls._repr_fields = () @@ -876,21 +899,37 @@ def __init__(self, value: object = _MISSING_INIT, /, **kwargs: object) -> None: raise TypeError(f"{type(self).__name__}() requires a value") if kwargs: raise TypeError(f"{type(self).__name__}() got unexpected kwargs: {sorted(kwargs)}") - self._arr = np.asarray(value, dtype=self.dtype) # ty: ignore[invalid-assignment] + if self._converter is not None: + arr = np.zeros((), dtype=self.dtype) + self._converter.encode_into(arr, value) + self._arr = arr + else: + self._arr = np.asarray(value, dtype=self.dtype) # ty: ignore[invalid-assignment] @classmethod def unwrap(cls, arr: "np.ndarray") -> Any: + if cls._converter is not None: + return cls._converter.decode_scalar(arr) # ty: ignore[invalid-argument-type] # 0-D → numpy scalar via item-like access (preserves dtype). # 1-D / sub-array → return the ndarray as-is. return arr if arr.ndim > 0 else arr[()] def _repr_kwargs(self) -> str: + if self._converter is not None: + return repr(self._converter.decode_scalar(self._arr)) # ty: ignore[invalid-argument-type] return repr(self._arr.tolist() if self._arr.ndim > 0 else self._arr[()]) def __repr__(self) -> str: return f"{type(self).__name__}({self._repr_kwargs()})" def to_dataframe(self, *, decode_enums: bool = True) -> pd.DataFrame: + if self._converter is not None: + arr = self._arr + # decode_batch wants a leading row axis; a single record lacks one + # (its ndim equals the converter slot's own ndim). + if arr.ndim == self._converter.dtype.ndim: + arr = arr[np.newaxis, ...] + return pd.DataFrame({"value": self._converter.decode_batch(arr)}) arr = np.atleast_1d(self._arr) # Sub-array dtype (array register): shape is already (N, length). if arr.ndim > 1: diff --git a/src/harp-protocol/harp/protocol/_register.py b/src/harp-protocol/harp/protocol/_register.py index c0496b2..258145d 100644 --- a/src/harp-protocol/harp/protocol/_register.py +++ b/src/harp-protocol/harp/protocol/_register.py @@ -212,7 +212,10 @@ def format( elif isinstance(value, np.ndarray): raw = value.tobytes() else: - raw = np.asarray(value, dtype=cls.payload_type.numpy_dtype).tobytes() + # A bare high-level value (the symmetric counterpart of what + # parse() returns): let the payload class encode it, so any + # converter (e.g. a str via StringConverter) is applied. + raw = cls.payload_class(value).raw_payload.tobytes() return build_message_frame( mt, cls.address, cls.payload_type, raw, port=port, timestamp=timestamp ) diff --git a/tests/protocol/test_register.py b/tests/protocol/test_register.py index 4854f71..a8f6aeb 100644 --- a/tests/protocol/test_register.py +++ b/tests/protocol/test_register.py @@ -18,7 +18,6 @@ PayloadU32, PayloadU64, Field, - BitFlag, _IdentityConverter, ) from harp.protocol._payload_type import PayloadType @@ -351,6 +350,65 @@ def test_structured_payload_descriptors_multi(): np.testing.assert_array_equal(parsed.analog_input1, [-200, -210, -220]) +def test_anonymous_payload_converter_roundtrip(): + """AnonymousPayload with a converter= encodes/decodes the single slot. + + Models a register that carries one value but needs a domain codec + (e.g. DeviceName -> StringConverter). + """ + from harp.protocol._payload import AnonymousPayload + from harp.protocol._payload_converters import StringConverter + + class PayloadDeviceName(AnonymousPayload, converter=StringConverter(25)): + pass + + class DeviceName(RegisterBase): + address: ClassVar[int] = 12 + payload_type: ClassVar[PayloadType] = PayloadType.U8 + payload_class = PayloadDeviceName + + # dtype derives from the converter; raw bytes are the encoded, null-padded value. + assert PayloadDeviceName.dtype == np.dtype((np.uint8, (25,))) + payload = PayloadDeviceName("Behavior") + assert payload.raw_payload.tobytes() == b"Behavior".ljust(25, b"\x00") + + # Register round-trip decodes back to the high-level str, whether format() + # is given a payload instance or the bare value (symmetric with parse()). + for arg in (payload, "Behavior"): + parsed = DeviceName.parse(_parse_frame(DeviceName.format(arg))) + assert parsed == "Behavior" + + # to_dataframe decodes both a single record and a batch. + assert PayloadDeviceName("Behavior").to_dataframe()["value"].tolist() == ["Behavior"] + two = ( + PayloadDeviceName("Foo").raw_payload.tobytes() + + PayloadDeviceName("Bar").raw_payload.tobytes() + ) + batch = PayloadDeviceName.from_buffer(two) + assert batch.to_dataframe()["value"].tolist() == ["Foo", "Bar"] + + +def test_anonymous_payload_scalar_converter_roundtrip(): + """A scalar (non-sub-array) converter= also round-trips through unwrap.""" + import enum + + from harp.protocol._payload import AnonymousPayload + from harp.protocol._payload_converters import EnumConverter + + class Color(enum.IntEnum): + RED = 0 + GREEN = 1 + BLUE = 2 + + class PayloadColor(AnonymousPayload, converter=EnumConverter(Color)): + pass + + assert PayloadColor.dtype == np.dtype(np.uint8) + raw = PayloadColor(Color.BLUE).raw_payload.tobytes() + record = np.frombuffer(raw, dtype=PayloadColor.dtype, count=1)[0] + assert PayloadColor.unwrap(record) == Color.BLUE + + def test_array_register_parse_returns_ndarray(): """parse() on an array register returns the 1-D ndarray directly (no .value).""" reg = RegisterU32Array(0x08, length=3) From c84f93eda5dcab0c870e7936dc05d5d8d884ef56 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Wed, 24 Jun 2026 14:21:19 -0700 Subject: [PATCH 233/267] Export `AnonymousPayload` from te public module --- src/harp-protocol/harp/protocol/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/harp-protocol/harp/protocol/__init__.py b/src/harp-protocol/harp/protocol/__init__.py index 78a22ea..6131403 100644 --- a/src/harp-protocol/harp/protocol/__init__.py +++ b/src/harp-protocol/harp/protocol/__init__.py @@ -31,6 +31,7 @@ PayloadS32Array, PayloadS64Array, PayloadFloatArray, + AnonymousPayload, ) from ._payload_type import PayloadType from ._register import ( @@ -73,6 +74,7 @@ # Payload DSL "PayloadBase", "StructPayload", + "AnonymousPayload", "Field", "BitFlag", "GroupMask", From 2589295005f431e6fb35fe9bafe4440740b9ff1b Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Wed, 24 Jun 2026 22:39:34 -0700 Subject: [PATCH 234/267] Add support for HarpVersion and HarpVersionConverter --- src/harp-protocol/harp/protocol/__init__.py | 4 ++ .../harp/protocol/_payload_converters.py | 36 ++++++++++++++++- tests/protocol/register_models.py | 39 +------------------ tests/protocol/test_register_modeling.py | 3 +- 4 files changed, 42 insertions(+), 40 deletions(-) diff --git a/src/harp-protocol/harp/protocol/__init__.py b/src/harp-protocol/harp/protocol/__init__.py index 6131403..7e4d497 100644 --- a/src/harp-protocol/harp/protocol/__init__.py +++ b/src/harp-protocol/harp/protocol/__init__.py @@ -6,6 +6,8 @@ EnumConverter, IdentityConverter, StringConverter, + HarpVersionConverter, + HarpVersion, ) from ._payload import ( PayloadBase, @@ -71,6 +73,8 @@ "StringConverter", "BoolConverter", "EnumConverter", + "HarpVersionConverter", + "HarpVersion", # Payload DSL "PayloadBase", "StructPayload", diff --git a/src/harp-protocol/harp/protocol/_payload_converters.py b/src/harp-protocol/harp/protocol/_payload_converters.py index 9255c7f..0cc8d0d 100644 --- a/src/harp-protocol/harp/protocol/_payload_converters.py +++ b/src/harp-protocol/harp/protocol/_payload_converters.py @@ -1,7 +1,7 @@ import enum as _enum from abc import ABC, abstractmethod from typing import Any, Generic, TypeVar, cast - +from dataclasses import dataclass import numpy as np from numpy.typing import NDArray @@ -171,3 +171,37 @@ def encode_into(self, view: NDArray[np.generic], value: str) -> None: encoded = value.encode(self._encoding)[: self._length] padded = encoded.ljust(self._length, b"\x00") view[...] = np.frombuffer(padded, dtype=np.uint8) + + +@dataclass(frozen=True) +class HarpVersion: + """Represents a Harp version""" + + major: int + minor: int + patch: int + + def __str__(self) -> str: + return f"{self.major}.{self.minor}.{self.patch}" + + +class HarpVersionConverter(Converter[HarpVersion]): + """Converts a 3-element uint8 array to/from a HarpVersion object.""" + + init_kwarg_type = HarpVersion + + def __init__(self, component: "np.dtype | str | type" = np.uint8) -> None: + self.dtype = np.dtype((component, (3,))) + + def decode_scalar(self, view: np.generic) -> HarpVersion: + c = np.asarray(view).tolist() + return HarpVersion(int(c[0]), int(c[1]), int(c[2])) + + def decode_batch(self, view: NDArray[np.generic]) -> Any: + return np.array( + [HarpVersion(int(r[0]), int(r[1]), int(r[2])) for r in np.atleast_2d(view)], + dtype=object, + ) + + def encode_into(self, view: NDArray[np.generic], value: HarpVersion) -> None: + view[...] = np.array([value.major, value.minor, value.patch], dtype=self.dtype.base) diff --git a/tests/protocol/register_models.py b/tests/protocol/register_models.py index 3ccfab2..b5e0748 100644 --- a/tests/protocol/register_models.py +++ b/tests/protocol/register_models.py @@ -26,7 +26,6 @@ from __future__ import annotations import enum -from dataclasses import dataclass from typing import Any, ClassVar import numpy as np @@ -40,6 +39,8 @@ GroupMask, HarpMessage, IdentityConverter, + HarpVersionConverter, + HarpVersion, PayloadType, RegisterBase, RegisterS32, @@ -88,42 +89,6 @@ class EncoderModeMask(enum.IntEnum): # =========================================================================== -@dataclass(frozen=True) -class HarpVersion: - """Illustrative domain type for ``interfaceType: HarpVersion`` (3 components).""" - - major: int - minor: int - patch: int - - def __str__(self) -> str: # pragma: no cover - cosmetic - return f"{self.major}.{self.minor}.{self.patch}" - - -class HarpVersionConverter(Converter[HarpVersion]): - """3 components <-> HarpVersion. Parameterized by component width, *not* by the - register: ``HarpVersionConverter(np.uint8)`` reads 3 bytes (Version members), - ``HarpVersionConverter(np.uint32)`` reads 12 bytes (CustomPayload register).""" - - init_kwarg_type = HarpVersion - - def __init__(self, component: "np.dtype | str | type" = np.uint8) -> None: - self.dtype = np.dtype((component, (3,))) - - def decode_scalar(self, view: np.generic) -> HarpVersion: - c = np.asarray(view).tolist() - return HarpVersion(int(c[0]), int(c[1]), int(c[2])) - - def decode_batch(self, view: NDArray[np.generic]) -> Any: - return np.array( - [HarpVersion(int(r[0]), int(r[1]), int(r[2])) for r in np.atleast_2d(view)], - dtype=object, - ) - - def encode_into(self, view: NDArray[np.generic], value: HarpVersion) -> None: - view[...] = np.array([value.major, value.minor, value.patch], dtype=self.dtype.base) - - class BytesToIntConverter(Converter[int]): """N raw bytes (little-endian) <-> Python int. Models ``interfaceType: int`` over a sub-array.""" diff --git a/tests/protocol/test_register_modeling.py b/tests/protocol/test_register_modeling.py index 9ea2dfb..2656d2e 100644 --- a/tests/protocol/test_register_modeling.py +++ b/tests/protocol/test_register_modeling.py @@ -5,7 +5,7 @@ import numpy as np import pytest -from harp.protocol import HarpMessage +from harp.protocol import HarpMessage, HarpVersion from tests.protocol.register_models import ( AnalogData, AnalogDataPayload, @@ -22,7 +22,6 @@ EncoderMode, EncoderModeMask, EncoderModePayload, - HarpVersion, PortDIOSet, PortDIOSetPayload, PulseDO0, From a1f9a7a64bd648254052609f328b7798f851b2df Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Wed, 24 Jun 2026 23:08:34 -0700 Subject: [PATCH 235/267] Refactor device class to include core registers and decoupled from serial transport --- scripts/device_integration.py | 3 +- src/harp-device/src/harp/device/_device.py | 228 +++++++++++++++++++-- src/harp-device/src/harp/device/_serial.py | 143 +++---------- 3 files changed, 245 insertions(+), 129 deletions(-) diff --git a/scripts/device_integration.py b/scripts/device_integration.py index 9bf205e..13628f9 100644 --- a/scripts/device_integration.py +++ b/scripts/device_integration.py @@ -14,6 +14,7 @@ HwVersionL, OperationControl, ResetDevice, + SerialDevice, SerialNumber, TimestampMicro, TimestampSecond, @@ -95,6 +96,6 @@ def latency_benchmark(dev: Device, n: int = N_READS) -> None: if __name__ == "__main__": # Live device reads (requires hardware) - with Device(PORT) as dev: + with SerialDevice(PORT) as dev: read_core_registers(dev) latency_benchmark(dev) diff --git a/src/harp-device/src/harp/device/_device.py b/src/harp-device/src/harp/device/_device.py index dc49c54..0fd229e 100644 --- a/src/harp-device/src/harp/device/_device.py +++ b/src/harp-device/src/harp/device/_device.py @@ -1,32 +1,232 @@ -"""High-level Harp device API.""" +"""Transport-agnostic Harp device base class.""" -from typing import Any, TypeVar +from typing import Any, ClassVar, Self, TypeVar +import queue +import threading +from abc import ABC, abstractmethod + +from harp.protocol import HarpMessage, MessageType from harp.protocol._message import ParsedHarpMessage from harp.protocol._register import RegisterBase -from ._serial import SerialDevice +from ._framer import HarpFramer +from ._registers import ( + AssemblyVersion, + ClockConfig, + CoreVersionH, + CoreVersionL, + DeviceName, + FirmwareVersionH, + FirmwareVersionL, + Heartbeat, + HwVersionH, + HwVersionL, + OperationControl, + ResetDevice, + SerialNumber, + TimestampMicro, + TimestampOffset, + TimestampSecond, + WhoAmI, +) P = TypeVar("P") -class Device: - def __init__(self, port: str, baudrate: int = SerialDevice.DEFAULT_BAUDRATE) -> None: - self._dev = SerialDevice(port, baudrate) +class Device(ABC): + """Transport-agnostic base for Harp devices. - def read(self, register: type[RegisterBase[P]]) -> ParsedHarpMessage[P]: - # Note: ty can't correctly infer the return type, and this is a known issue: - # https://github.com/astral-sh/ty/issues/623 - return self._dev.read_register(register) + Owns the protocol logic (framing, request/reply, register access). + Subclasses supply a transport via the abstract :meth:`_write`/:meth:`_read` + and the optional :meth:`_open`/:meth:`_close` hooks; they store transport + config then call ``super().__init__()``, which connects and starts reading. + + Common registers are exposed as class attributes (``dev.read(dev.WhoAmI)``). + Set :attr:`__whoami__` to have ``open()`` validate the device identity + (``0x0`` skips the check). + """ + + REPLY_TIMEOUT: ClassVar[float] = 5.0 # seconds + + #: Expected ``WhoAmI`` of the device this class models; ``0x0`` skips the check. + __whoami__: ClassVar[int] = 0x0 + + # -- exposed registers -------------------------------------------------- + WhoAmI = WhoAmI + HwVersionH = HwVersionH + HwVersionL = HwVersionL + AssemblyVersion = AssemblyVersion + CoreVersionH = CoreVersionH + CoreVersionL = CoreVersionL + FirmwareVersionH = FirmwareVersionH + FirmwareVersionL = FirmwareVersionL + TimestampSecond = TimestampSecond + TimestampMicro = TimestampMicro + OperationControl = OperationControl + ResetDevice = ResetDevice + DeviceName = DeviceName + SerialNumber = SerialNumber + TimestampOffset = TimestampOffset + ClockConfig = ClockConfig + Heartbeat = Heartbeat + + def __init__(self, *, raise_on_error: bool = True) -> None: + self.raise_on_error = raise_on_error + self._framer = HarpFramer() + self._pending: dict[int, queue.SimpleQueue] = {} + self._pending_lock = threading.Lock() + self._running = False + self._thread: threading.Thread | None = None + self.open() + + # ------------------------------------------------------------------ + # Transport primitives (implemented by subclasses) + # ------------------------------------------------------------------ + + def _open(self) -> None: + """Open the underlying transport. Default: no-op.""" + + @abstractmethod + def _write(self, data: bytes) -> None: + """Send raw bytes over the transport.""" + + @abstractmethod + def _read(self) -> bytes: + """Return available bytes, or ``b''`` on timeout/idle. + + Block only briefly so the read loop can poll ``_running``, and return + ``b''`` rather than raise once the transport is closing. + """ + + def _close(self) -> None: + """Close the underlying transport. Default: no-op.""" - def write(self, register: type[RegisterBase[P]], value: Any) -> ParsedHarpMessage[P]: - return self._dev.write_register(register, value) + # ------------------------------------------------------------------ + # Lifecycle + # ------------------------------------------------------------------ + + def open(self) -> Self: + self._open() + self._running = True + self._thread = threading.Thread( + target=self._read_loop, daemon=True, name=f"{type(self).__name__}-reader" + ) + self._thread.start() + try: + self._validate_whoami() + except Exception: + self.close() + raise + return self + + def _validate_whoami(self) -> None: + """Check the device's ``WhoAmI`` against :attr:`__whoami__` (``0x0`` skips).""" + expected = self.__whoami__ + if expected == 0x0: + return + actual = int(self.read(WhoAmI).parsed) + if actual != expected: + raise RuntimeError( + f"WhoAmI mismatch: {type(self).__name__} expected 0x{expected:04x} " + f"but device reported 0x{actual:04x}." + ) def close(self) -> None: - self._dev.close() + self._running = False + if self._thread is not None: + self._thread.join(timeout=2.0) + self._thread = None + self._close() - def __enter__(self) -> "Device": + def __enter__(self) -> Self: return self def __exit__(self, *args: object) -> None: self.close() + + # ------------------------------------------------------------------ + # Register access + # ------------------------------------------------------------------ + + def read( + self, + register: type[RegisterBase[P]], + *, + timestamp: float | None = None, + port: int = 255, + ) -> ParsedHarpMessage[P]: + # Note: ty can't correctly infer the return type, and this is a known issue: + # https://github.com/astral-sh/ty/issues/623 + frame = register.format(message_type=MessageType.Read, timestamp=timestamp, port=port) + msg = self._request(register.address, frame) + return ParsedHarpMessage.from_message(msg, register.parse(msg)) + + def write( + self, + register: type[RegisterBase[P]], + value: Any, + *, + timestamp: float | None = None, + port: int = 255, + ) -> ParsedHarpMessage[P]: + frame = register.format( + value, message_type=MessageType.Write, timestamp=timestamp, port=port + ) + msg = self._request(register.address, frame) + return ParsedHarpMessage.from_message(msg, register.parse(msg)) + + # ------------------------------------------------------------------ + # Internals + # ------------------------------------------------------------------ + + def _on_event(self, msg: HarpMessage) -> None: + """Handle an Event message from the reader thread. Default: discard.""" + + def _read_loop(self) -> None: + while self._running: + chunk = self._read() + if not chunk: + continue + + self._framer.feed(chunk) + for msg in self._framer.frames(): + self._dispatch(msg) + + def _dispatch(self, msg: HarpMessage) -> None: + if msg.message_type in (MessageType.Read, MessageType.Write): + with self._pending_lock: + q = self._pending.get(msg.address) + if q is not None: + q.put(msg) + elif msg.message_type == MessageType.Event: + self._on_event(msg) + else: + # Unknown message type + with self._pending_lock: + q = self._pending.get(msg.address) + if q is not None: + q.put(msg) + + def _request(self, address: int, frame: bytes) -> HarpMessage: + q: queue.SimpleQueue = queue.SimpleQueue() + with self._pending_lock: + self._pending[address] = q + try: + self._write(frame) + try: + msg = q.get(timeout=self.REPLY_TIMEOUT) + if msg.has_error and self.raise_on_error: + raise RuntimeError( + f"Device returned error for register address {address} " + f"(0x{address:02x}). Payload: {msg.payload.hex()}" + ) + return msg + except queue.Empty as exc: + raise TimeoutError( + f"No reply from device for register address {address} " + f"within {self.REPLY_TIMEOUT}s" + ) from exc + finally: + with self._pending_lock: + self._pending.pop(address, None) diff --git a/src/harp-device/src/harp/device/_serial.py b/src/harp-device/src/harp/device/_serial.py index 7a8946a..debff7a 100644 --- a/src/harp-device/src/harp/device/_serial.py +++ b/src/harp-device/src/harp/device/_serial.py @@ -1,131 +1,46 @@ -import queue -import threading -from typing import Any, ClassVar, Self, TypeVar +from typing import ClassVar import serial -from harp.device._framer import HarpFramer -from harp.protocol import HarpMessage, MessageType -from harp.protocol._message import ParsedHarpMessage -from harp.protocol._register import RegisterBase -P = TypeVar("P") +from ._device import Device -class SerialDevice: +class SerialDevice(Device): + """A :class:`Device` whose transport is a serial port.""" + DEFAULT_BAUDRATE: ClassVar[int] = 1_000_000 - REPLY_TIMEOUT: float = 5.0 # seconds def __init__( - self, port: str, baudrate: int = DEFAULT_BAUDRATE, raise_on_error: bool = True + self, port: str, baudrate: int = DEFAULT_BAUDRATE, *, raise_on_error: bool = True ) -> None: - self._serial = serial.Serial(port, baudrate, timeout=0.1) - self._serial.dtr = True - - self._framer = HarpFramer() - self._pending: dict[int, queue.SimpleQueue] = {} - self._pending_lock = threading.Lock() - self._running = True - self.raise_on_error = raise_on_error - - self._thread = threading.Thread( - target=self._read_loop, daemon=True, name="harp-serial-device" - ) - self._thread.start() + self._port = port + self._baudrate = baudrate + self._serial: serial.Serial | None = None + super().__init__(raise_on_error=raise_on_error) - def read_register( - self, register: type[RegisterBase[P]], *, timestamp: float | None = None, port: int = 255 - ) -> ParsedHarpMessage[P]: - frame = register.format(message_type=MessageType.Read, timestamp=timestamp, port=port) - msg = self._request(register.address, frame) - return ParsedHarpMessage.from_message(msg, register.parse(msg)) + def _open(self) -> None: + self._serial = serial.Serial(self._port, self._baudrate, timeout=0.1) + self._serial.dtr = True - def write_register( - self, - register: type[RegisterBase[P]], - value: Any, - *, - timestamp: float | None = None, - port: int = 255, - ) -> ParsedHarpMessage[P]: - frame = register.format( - value, message_type=MessageType.Write, timestamp=timestamp, port=port - ) - msg = self._request(register.address, frame) - return ParsedHarpMessage.from_message(msg, register.parse(msg)) + def _write(self, data: bytes) -> None: + assert self._serial is not None + self._serial.write(data) - def close(self) -> None: - self._running = False - self._thread.join(timeout=2.0) + def _read(self) -> bytes: + assert self._serial is not None + try: + waiting = self._serial.in_waiting + return self._serial.read(waiting if waiting > 0 else 1) + except serial.SerialException: + if self._running: + raise + return b"" + + def _close(self) -> None: + if self._serial is None: + return try: self._serial.dtr = False except Exception: pass self._serial.close() - - def __enter__(self) -> Self: - return self - - def __exit__(self, *args: object) -> None: - self.close() - - def _on_event(self, msg: HarpMessage) -> None: - """Called from the reader thread when an Event message arrives. - - Override in subclasses to handle device-specific events. - The default implementation discards the message. - """ - - def _read_loop(self) -> None: - while self._running: - try: - waiting = self._serial.in_waiting - chunk = self._serial.read(waiting if waiting > 0 else 1) - except serial.SerialException: - if self._running: - raise - break - - if not chunk: - continue - - self._framer.feed(chunk) - for msg in self._framer.frames(): - self._dispatch(msg) - - def _dispatch(self, msg: HarpMessage) -> None: - if msg.message_type in (MessageType.Read, MessageType.Write): - with self._pending_lock: - q = self._pending.get(msg.address) - if q is not None: - q.put(msg) - elif msg.message_type == MessageType.Event: - self._on_event(msg) - else: - # Unknown message type - with self._pending_lock: - q = self._pending.get(msg.address) - if q is not None: - q.put(msg) - - def _request(self, address: int, frame: bytes) -> HarpMessage: - q: queue.SimpleQueue = queue.SimpleQueue() - with self._pending_lock: - self._pending[address] = q - try: - self._serial.write(frame) - try: - msg = q.get(timeout=self.REPLY_TIMEOUT) - if msg.has_error and self.raise_on_error: - raise RuntimeError( - f"Device returned error for register address {address} " - f"(0x{address:02x}). Payload: {msg.payload.hex()}" - ) - return msg - except queue.Empty as exc: - raise TimeoutError( - f"No reply from device for register address {address} " - f"within {self.REPLY_TIMEOUT}s" - ) from exc - finally: - with self._pending_lock: - self._pending.pop(address, None) From 4ad934310b9b7cd8c5b80797e8cb56a98ff99298 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Thu, 25 Jun 2026 02:14:50 -0700 Subject: [PATCH 236/267] Decouple transport from device --- scripts/device_integration.py | 4 +- src/harp-device/src/harp/device/__init__.py | 8 ++- src/harp-device/src/harp/device/_device.py | 60 +++++++----------- src/harp-device/src/harp/device/_serial.py | 63 +++++++++++++------ src/harp-device/src/harp/device/_transport.py | 25 ++++++++ 5 files changed, 98 insertions(+), 62 deletions(-) create mode 100644 src/harp-device/src/harp/device/_transport.py diff --git a/scripts/device_integration.py b/scripts/device_integration.py index 13628f9..dc519d9 100644 --- a/scripts/device_integration.py +++ b/scripts/device_integration.py @@ -14,11 +14,11 @@ HwVersionL, OperationControl, ResetDevice, - SerialDevice, SerialNumber, TimestampMicro, TimestampSecond, WhoAmI, + open_serial_device, ) PORT = "COM4" @@ -96,6 +96,6 @@ def latency_benchmark(dev: Device, n: int = N_READS) -> None: if __name__ == "__main__": # Live device reads (requires hardware) - with SerialDevice(PORT) as dev: + with open_serial_device(Device, port=PORT) as dev: read_core_registers(dev) latency_benchmark(dev) diff --git a/src/harp-device/src/harp/device/__init__.py b/src/harp-device/src/harp/device/__init__.py index d7fc0b8..892a838 100644 --- a/src/harp-device/src/harp/device/__init__.py +++ b/src/harp-device/src/harp/device/__init__.py @@ -25,12 +25,16 @@ TimestampSecond, WhoAmI, ) -from ._serial import SerialDevice +from ._serial import SerialTransport, open_serial_device +from ._transport import ITransport, TransportError __all__ = [ "Device", "HarpFramer", - "SerialDevice", + "ITransport", + "TransportError", + "SerialTransport", + "open_serial_device", "WhoAmI", "HwVersionH", "HwVersionL", diff --git a/src/harp-device/src/harp/device/_device.py b/src/harp-device/src/harp/device/_device.py index 0fd229e..92770e3 100644 --- a/src/harp-device/src/harp/device/_device.py +++ b/src/harp-device/src/harp/device/_device.py @@ -4,13 +4,13 @@ import queue import threading -from abc import ABC, abstractmethod from harp.protocol import HarpMessage, MessageType from harp.protocol._message import ParsedHarpMessage from harp.protocol._register import RegisterBase from ._framer import HarpFramer +from ._transport import ITransport, TransportError from ._registers import ( AssemblyVersion, ClockConfig, @@ -34,17 +34,13 @@ P = TypeVar("P") -class Device(ABC): - """Transport-agnostic base for Harp devices. +class Device: + """Harp device protocol logic (framing, request/reply, register access) + over an :class:`~harp.device.ITransport`. - Owns the protocol logic (framing, request/reply, register access). - Subclasses supply a transport via the abstract :meth:`_write`/:meth:`_read` - and the optional :meth:`_open`/:meth:`_close` hooks; they store transport - config then call ``super().__init__()``, which connects and starts reading. - - Common registers are exposed as class attributes (``dev.read(dev.WhoAmI)``). - Set :attr:`__whoami__` to have ``open()`` validate the device identity - (``0x0`` skips the check). + Must be opened before use, via ``with`` or :meth:`open`. Subclasses add + register class attributes and set :attr:`__whoami__` to validate device + identity on open (``0x0`` skips the check). """ REPLY_TIMEOUT: ClassVar[float] = 5.0 # seconds @@ -71,43 +67,22 @@ class Device(ABC): ClockConfig = ClockConfig Heartbeat = Heartbeat - def __init__(self, *, raise_on_error: bool = True) -> None: + def __init__(self, transport: ITransport, *, raise_on_error: bool = True) -> None: + self._transport = transport self.raise_on_error = raise_on_error self._framer = HarpFramer() self._pending: dict[int, queue.SimpleQueue] = {} self._pending_lock = threading.Lock() self._running = False self._thread: threading.Thread | None = None - self.open() - - # ------------------------------------------------------------------ - # Transport primitives (implemented by subclasses) - # ------------------------------------------------------------------ - - def _open(self) -> None: - """Open the underlying transport. Default: no-op.""" - - @abstractmethod - def _write(self, data: bytes) -> None: - """Send raw bytes over the transport.""" - - @abstractmethod - def _read(self) -> bytes: - """Return available bytes, or ``b''`` on timeout/idle. - - Block only briefly so the read loop can poll ``_running``, and return - ``b''`` rather than raise once the transport is closing. - """ - - def _close(self) -> None: - """Close the underlying transport. Default: no-op.""" # ------------------------------------------------------------------ # Lifecycle # ------------------------------------------------------------------ def open(self) -> Self: - self._open() + """Open the transport, start the reader thread and validate identity.""" + self._transport.open() self._running = True self._thread = threading.Thread( target=self._read_loop, daemon=True, name=f"{type(self).__name__}-reader" @@ -137,9 +112,11 @@ def close(self) -> None: if self._thread is not None: self._thread.join(timeout=2.0) self._thread = None - self._close() + self._transport.close() def __enter__(self) -> Self: + if not self._running: + self.open() return self def __exit__(self, *args: object) -> None: @@ -185,7 +162,12 @@ def _on_event(self, msg: HarpMessage) -> None: def _read_loop(self) -> None: while self._running: - chunk = self._read() + try: + chunk = self._transport.read() + except TransportError: + if self._running: + raise + break # expected while shutting down if not chunk: continue @@ -213,7 +195,7 @@ def _request(self, address: int, frame: bytes) -> HarpMessage: with self._pending_lock: self._pending[address] = q try: - self._write(frame) + self._transport.write(frame) try: msg = q.get(timeout=self.REPLY_TIMEOUT) if msg.has_error and self.raise_on_error: diff --git a/src/harp-device/src/harp/device/_serial.py b/src/harp-device/src/harp/device/_serial.py index debff7a..1263d51 100644 --- a/src/harp-device/src/harp/device/_serial.py +++ b/src/harp-device/src/harp/device/_serial.py @@ -1,42 +1,48 @@ -from typing import ClassVar +"""Serial transport and factory for Harp devices.""" + +from typing import TypeVar import serial from ._device import Device +from ._transport import TransportError + +D = TypeVar("D", bound=Device) +DEFAULT_BAUDRATE: int = 1_000_000 -class SerialDevice(Device): - """A :class:`Device` whose transport is a serial port.""" - DEFAULT_BAUDRATE: ClassVar[int] = 1_000_000 +class SerialTransport: + """A serial-port :class:`~harp.device.ITransport` (structural conformance).""" - def __init__( - self, port: str, baudrate: int = DEFAULT_BAUDRATE, *, raise_on_error: bool = True - ) -> None: + def __init__(self, port: str, baudrate: int = DEFAULT_BAUDRATE) -> None: self._port = port self._baudrate = baudrate self._serial: serial.Serial | None = None - super().__init__(raise_on_error=raise_on_error) - def _open(self) -> None: - self._serial = serial.Serial(self._port, self._baudrate, timeout=0.1) - self._serial.dtr = True + def open(self) -> None: + try: + self._serial = serial.Serial(self._port, self._baudrate, timeout=0.1) + self._serial.dtr = True + except serial.SerialException as exc: + raise TransportError(f"Failed to open serial port {self._port!r}: {exc}") from exc - def _write(self, data: bytes) -> None: + def write(self, data: bytes) -> None: assert self._serial is not None - self._serial.write(data) + try: + self._serial.write(data) + except serial.SerialException as exc: + raise TransportError(str(exc)) from exc - def _read(self) -> bytes: + def read(self) -> bytes: assert self._serial is not None try: waiting = self._serial.in_waiting return self._serial.read(waiting if waiting > 0 else 1) - except serial.SerialException: - if self._running: - raise - return b"" + except serial.SerialException as exc: + raise TransportError(str(exc)) from exc - def _close(self) -> None: + def close(self) -> None: if self._serial is None: return try: @@ -44,3 +50,22 @@ def _close(self) -> None: except Exception: pass self._serial.close() + + +def open_serial_device( + device: type[D], + *, + port: str, + baudrate: int = DEFAULT_BAUDRATE, + raise_on_error: bool = True, +) -> D: + """Build ``device`` over a serial transport and open it. + + Like the builtin :func:`open`, the returned device is already connected; + use it directly or in a ``with`` block for guaranteed close:: + + with open_serial_device(behavior.Device, port="COM3") as dev: + dev.read(behavior.WhoAmI) + """ + transport = SerialTransport(port, baudrate) + return device(transport, raise_on_error=raise_on_error).open() diff --git a/src/harp-device/src/harp/device/_transport.py b/src/harp-device/src/harp/device/_transport.py new file mode 100644 index 0000000..efa4dfc --- /dev/null +++ b/src/harp-device/src/harp/device/_transport.py @@ -0,0 +1,25 @@ +"""Byte transport abstraction for Harp devices.""" + +from typing import Protocol, runtime_checkable + + +class TransportError(Exception): + """Raised by a transport when the underlying byte channel fails.""" + + +@runtime_checkable +class ITransport(Protocol): + """Byte channel a :class:`~harp.device.Device` drives. + + Owns no protocol logic. Failures are reported as :class:`TransportError`. + """ + + def open(self) -> None: ... + + def write(self, data: bytes) -> None: ... + + def read(self) -> bytes: + """Return available bytes, or ``b''`` on idle/timeout.""" + ... + + def close(self) -> None: ... From 8068d724620d5b7bbf0802f5f974b07767f6fb0e Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Thu, 25 Jun 2026 02:15:34 -0700 Subject: [PATCH 237/267] Use automatically generated core register definition --- src/harp-device/src/harp/device/_registers.py | 255 +++++++++++------- 1 file changed, 165 insertions(+), 90 deletions(-) diff --git a/src/harp-device/src/harp/device/_registers.py b/src/harp-device/src/harp/device/_registers.py index 0b86ee4..85eeb5e 100644 --- a/src/harp-device/src/harp/device/_registers.py +++ b/src/harp-device/src/harp/device/_registers.py @@ -1,155 +1,230 @@ +# This file was automatically generated and should not be edited directly. +# To make changes, edit the device metadata and regenerate the interface. + import enum from typing import ClassVar import numpy as np -from harp.protocol._payload import AnonymousPayload, BitFlag, GroupMask, StructPayload -from harp.protocol._payload_converters import StringConverter as _StringConverter -from harp.protocol._payload_type import PayloadType -from harp.protocol._register import RegisterBase, RegisterU8, RegisterU16, RegisterU32 +from harp.protocol import ( + AnonymousPayload, + BitFlag, + GroupMask, + PayloadType, + RegisterBase, + RegisterU16, + RegisterU32, + RegisterU8, + StringConverter, + StructPayload, +) + + +class ResetFlags(enum.IntFlag): + """Specifies the behavior of the non-volatile registers when resetting the device.""" + + RESTORE_DEFAULT = 0x1 + """The device will boot with all the registers reset to their default factory values.""" + RESTORE_EEPROM = 0x2 + """The device will boot and restore all the registers to the values stored in non-volatile memory.""" + SAVE = 0x4 + """The device will boot and save all the current register values to non-volatile memory.""" + RESTORE_NAME = 0x8 + """The device will boot with the default device name.""" + UPDATE_FIRMWARE = 0x20 + """The device will enter firmware update mode.""" + BOOT_FROM_DEFAULT = 0x40 + """Specifies that the device has booted from default factory values.""" + BOOT_FROM_EEPROM = 0x80 + """Specifies that the device has booted from non-volatile values stored in EEPROM.""" + + +class ClockConfigurationFlags(enum.IntFlag): + """Specifies configuration flags for the device synchronization clock.""" + + CLOCK_REPEATER = 0x1 + """The device will repeat the clock synchronization signal to the clock output connector, if available.""" + CLOCK_GENERATOR = 0x2 + """The device resets and generates the clock synchronization signal on the clock output connector, if available.""" + REPEATER_CAPABILITY = 0x8 + """Specifies the device has the capability to repeat the clock synchronization signal to the clock output connector.""" + GENERATOR_CAPABILITY = 0x10 + """Specifies the device has the capability to generate the clock synchronization signal to the clock output connector.""" + CLOCK_UNLOCK = 0x40 + """The device will unlock the timestamp register counter and will accept commands to set new timestamp values.""" + CLOCK_LOCK = 0x80 + """The device will lock the timestamp register counter and will not accept commands to set new timestamp values.""" class OperationMode(enum.IntEnum): """Specifies the operation mode of the device.""" - Standby = 0 - Active = 1 - Speed = 3 + STANDBY = 0 + """Disable all event reporting on the device.""" + ACTIVE = 1 + """Event detection is enabled. Only enabled events are reported by the device.""" + SPEED = 3 + """The device enters speed mode.""" class EnableFlag(enum.IntEnum): """Specifies whether a specific register flag is enabled or disabled.""" - Disabled = 0 - Enabled = 1 - - -# --------------------------------------------------------------------------- -# Complex payload classes -# --------------------------------------------------------------------------- + DISABLED = 0 + """Specifies that the flag is disabled.""" + ENABLED = 1 + """Specifies that the flag is enabled.""" class OperationControlPayload(StructPayload[np.uint8]): - operation_mode: OperationMode = GroupMask( - mask=0x03, enum=OperationMode, default=OperationMode.Standby - ) - dump_registers: bool = BitFlag(mask=0x08, default=False) - mute_replies: bool = BitFlag(mask=0x10, default=False) - visual_indicators: EnableFlag = GroupMask( - mask=0x20, enum=EnableFlag, default=EnableFlag.Disabled - ) - operation_led: EnableFlag = GroupMask(mask=0x40, enum=EnableFlag, default=EnableFlag.Disabled) - heartbeat: EnableFlag = GroupMask(mask=0x80, enum=EnableFlag, default=EnableFlag.Disabled) + """Represents the payload of the OperationControl register.""" + + operation_mode: OperationMode = GroupMask(enum=OperationMode, mask=0x3) + """Specifies the operation mode of the device.""" + dump_registers: bool = BitFlag(mask=0x8) + """Specifies whether the device should report the content of all registers on initialization.""" + mute_replies: bool = BitFlag(mask=0x10) + """Specifies whether the replies to all commands will be muted, i.e. not sent by the device.""" + visual_indicators: EnableFlag = GroupMask(enum=EnableFlag, mask=0x20) + """Specifies the state of all visual indicators on the device.""" + operation_led: EnableFlag = GroupMask(enum=EnableFlag, mask=0x40) + """Specifies whether the device state LED should report the operation mode of the device.""" + heartbeat: EnableFlag = GroupMask(enum=EnableFlag, mask=0x80) + """Specifies whether the device should report the content of the seconds register each second.""" class ResetDevicePayload(StructPayload[np.uint8]): - """Payload for the ResetDevice register (address 11).""" - - restore_default: bool = BitFlag(mask=0x01, default=False) - restore_eeprom: bool = BitFlag(mask=0x02, default=False) - save: bool = BitFlag(mask=0x04, default=False) - restore_name: bool = BitFlag(mask=0x08, default=False) - update_firmware: bool = BitFlag(mask=0x20, default=False) - boot_from_default: bool = BitFlag(mask=0x40, default=False) - boot_from_eeprom: bool = BitFlag(mask=0x80, default=False) + """Represents the payload of the ResetDevice register.""" + + restore_default: bool = BitFlag(mask=0x1) + """The device will boot with all the registers reset to their default factory values.""" + restore_eeprom: bool = BitFlag(mask=0x2) + """The device will boot and restore all the registers to the values stored in non-volatile memory.""" + save: bool = BitFlag(mask=0x4) + """The device will boot and save all the current register values to non-volatile memory.""" + restore_name: bool = BitFlag(mask=0x8) + """The device will boot with the default device name.""" + update_firmware: bool = BitFlag(mask=0x20) + """The device will enter firmware update mode.""" + boot_from_default: bool = BitFlag(mask=0x40) + """Specifies that the device has booted from default factory values.""" + boot_from_eeprom: bool = BitFlag(mask=0x80) + """Specifies that the device has booted from non-volatile values stored in EEPROM.""" + + +class DeviceNamePayload(AnonymousPayload, converter=StringConverter(25)): + """Represents the payload of the DeviceName register.""" + + +class ClockConfigurationPayload(StructPayload[np.uint8]): + """Represents the payload of the ClockConfiguration register.""" + + clock_repeater: bool = BitFlag(mask=0x1) + """The device will repeat the clock synchronization signal to the clock output connector, if available.""" + clock_generator: bool = BitFlag(mask=0x2) + """The device resets and generates the clock synchronization signal on the clock output connector, if available.""" + repeater_capability: bool = BitFlag(mask=0x8) + """Specifies the device has the capability to repeat the clock synchronization signal to the clock output connector.""" + generator_capability: bool = BitFlag(mask=0x10) + """Specifies the device has the capability to generate the clock synchronization signal to the clock output connector.""" + clock_unlock: bool = BitFlag(mask=0x40) + """The device will unlock the timestamp register counter and will accept commands to set new timestamp values.""" + clock_lock: bool = BitFlag(mask=0x80) + """The device will lock the timestamp register counter and will not accept commands to set new timestamp values.""" -class DeviceNamePayload(AnonymousPayload, converter=_StringConverter(25)): - """Payload for the DeviceName register (address 12). +class WhoAmI(RegisterU16): + """Specifies the identity class of the device.""" - Stores a user-specified ASCII device name padded to 25 bytes. - """ + address: ClassVar[int] = 0 -class ClockConfigPayload(StructPayload[np.uint8]): - """Payload for the ClockConfiguration register (address 14).""" +class HardwareVersionHigh(RegisterU8): + """Specifies the major hardware version of the device.""" - clock_repeater: bool = BitFlag(mask=0x01, default=False) - clock_generator: bool = BitFlag(mask=0x02, default=False) - repeater_capability: bool = BitFlag(mask=0x08, default=False) - generator_capability: bool = BitFlag(mask=0x10, default=False) - clock_unlock: bool = BitFlag(mask=0x40, default=False) - clock_lock: bool = BitFlag(mask=0x80, default=False) + address: ClassVar[int] = 1 -# --------------------------------------------------------------------------- -# Registers -# --------------------------------------------------------------------------- +class HardwareVersionLow(RegisterU8): + """Specifies the minor hardware version of the device.""" + address: ClassVar[int] = 2 -class WhoAmI(RegisterU16): - address: ClassVar[int] = 0 +class AssemblyVersion(RegisterU8): + """Specifies the version of the assembled components in the device.""" -class TimestampSecond(RegisterU32): - address: ClassVar[int] = 8 + address: ClassVar[int] = 3 -class TimestampMicro(RegisterU16): - address: ClassVar[int] = 9 +class CoreVersionHigh(RegisterU8): + """Specifies the major version of the Harp core implemented by the device.""" + address: ClassVar[int] = 4 -class OperationControl(RegisterBase[OperationControlPayload]): - address: ClassVar[int] = 10 - payload_type: ClassVar[PayloadType] = PayloadType.U8 - payload_class = OperationControlPayload +class CoreVersionLow(RegisterU8): + """Specifies the minor version of the Harp core implemented by the device.""" -class ResetDevice(RegisterBase[ResetDevicePayload]): - address: ClassVar[int] = 11 - payload_type: ClassVar[PayloadType] = PayloadType.U8 - payload_class = ResetDevicePayload - + address: ClassVar[int] = 5 -class DeviceName(RegisterBase[str]): - address: ClassVar[int] = 12 - payload_type: ClassVar[PayloadType] = PayloadType.U8 - payload_class = DeviceNamePayload +class FirmwareVersionHigh(RegisterU8): + """Specifies the major version of the Harp core implemented by the device.""" -class ClockConfig(RegisterBase[ClockConfigPayload]): - address: ClassVar[int] = 14 - payload_type: ClassVar[PayloadType] = PayloadType.U8 - payload_class = ClockConfigPayload + address: ClassVar[int] = 6 -class Heartbeat(RegisterU16): - address: ClassVar[int] = 18 +class FirmwareVersionLow(RegisterU8): + """Specifies the minor version of the Harp core implemented by the device.""" + address: ClassVar[int] = 7 -# ── Deprecated ─────────────────────── +class TimestampSeconds(RegisterU32): + """Stores the integral part of the system timestamp, in seconds.""" -class HwVersionH(RegisterU8): - address: ClassVar[int] = 1 + address: ClassVar[int] = 8 -class HwVersionL(RegisterU8): - address: ClassVar[int] = 2 +class TimestampMicroseconds(RegisterU16): + """Stores the fractional part of the system timestamp, in microseconds.""" + address: ClassVar[int] = 9 -class AssemblyVersion(RegisterU8): - address: ClassVar[int] = 3 +class OperationControl(RegisterBase[OperationControlPayload]): + """Stores the configuration mode of the device.""" -class CoreVersionH(RegisterU8): - address: ClassVar[int] = 4 + address: ClassVar[int] = 10 + payload_type: ClassVar[PayloadType] = PayloadType.U8 + payload_class = OperationControlPayload -class CoreVersionL(RegisterU8): - address: ClassVar[int] = 5 +class ResetDevice(RegisterBase[ResetDevicePayload]): + """Resets the device and saves non-volatile registers.""" + address: ClassVar[int] = 11 + payload_type: ClassVar[PayloadType] = PayloadType.U8 + payload_class = ResetDevicePayload -class FirmwareVersionH(RegisterU8): - address: ClassVar[int] = 6 +class DeviceName(RegisterBase[str]): + """Stores the user-specified device name.""" -class FirmwareVersionL(RegisterU8): - address: ClassVar[int] = 7 + address: ClassVar[int] = 12 + payload_type: ClassVar[PayloadType] = PayloadType.U8 + payload_class = DeviceNamePayload class SerialNumber(RegisterU16): + """Specifies the unique serial number of the device.""" + address: ClassVar[int] = 13 -class TimestampOffset(RegisterU8): - address: ClassVar[int] = 15 +class ClockConfiguration(RegisterBase[ClockConfigurationPayload]): + """Specifies the configuration for the device synchronization clock.""" + + address: ClassVar[int] = 14 + payload_type: ClassVar[PayloadType] = PayloadType.U8 + payload_class = ClockConfigurationPayload From de78f56ce06dc85e40a14ac7014898c4b45e59c6 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Thu, 25 Jun 2026 02:46:08 -0700 Subject: [PATCH 238/267] Split serial from device package --- docs/api/protocol.md | 2 +- docs/api/serial.md | 2 +- pyproject.toml | 12 +- scripts/core_registers_example.py | 62 ++++--- scripts/device_integration.py | 42 ++--- src/{ => packages}/harp-device/pyproject.toml | 3 +- .../harp-device/src/harp/device/__init__.py | 51 +++--- .../harp-device/src/harp/device/_device.py | 40 +++-- .../harp-device/src/harp/device/_framer.py | 0 .../harp-device/src/harp/device/_registers.py | 0 .../harp-device/src/harp/device/_transport.py | 0 .../harp-device/src/harp/device/py.typed | 0 src/{ => packages}/harp-protocol/README.md | 0 .../harp-protocol/pyproject.toml | 2 +- .../src}/harp/protocol/__init__.py | 0 .../src}/harp/protocol/_builder.py | 0 .../src}/harp/protocol/_checksum.py | 0 .../src}/harp/protocol/_constants.py | 0 .../src}/harp/protocol/_message.py | 0 .../src}/harp/protocol/_message_type.py | 0 .../src}/harp/protocol/_payload.py | 0 .../src}/harp/protocol/_payload_converters.py | 0 .../src}/harp/protocol/_payload_type.py | 0 .../src}/harp/protocol/_register.py | 0 .../harp-protocol/src}/harp/protocol/py.typed | 0 src/packages/harp-serial/pyproject.toml | 18 +++ .../harp-serial/src/harp/serial/__init__.py | 6 + .../harp-serial/src/harp/serial}/_serial.py | 3 +- tests/test_device.py | 34 ++-- uv.lock | 152 ++++++++++-------- 30 files changed, 231 insertions(+), 198 deletions(-) rename src/{ => packages}/harp-device/pyproject.toml (74%) rename src/{ => packages}/harp-device/src/harp/device/__init__.py (53%) rename src/{ => packages}/harp-device/src/harp/device/_device.py (91%) rename src/{ => packages}/harp-device/src/harp/device/_framer.py (100%) rename src/{ => packages}/harp-device/src/harp/device/_registers.py (100%) rename src/{ => packages}/harp-device/src/harp/device/_transport.py (100%) rename src/{ => packages}/harp-device/src/harp/device/py.typed (100%) rename src/{ => packages}/harp-protocol/README.md (100%) rename src/{ => packages}/harp-protocol/pyproject.toml (96%) rename src/{harp-protocol => packages/harp-protocol/src}/harp/protocol/__init__.py (100%) rename src/{harp-protocol => packages/harp-protocol/src}/harp/protocol/_builder.py (100%) rename src/{harp-protocol => packages/harp-protocol/src}/harp/protocol/_checksum.py (100%) rename src/{harp-protocol => packages/harp-protocol/src}/harp/protocol/_constants.py (100%) rename src/{harp-protocol => packages/harp-protocol/src}/harp/protocol/_message.py (100%) rename src/{harp-protocol => packages/harp-protocol/src}/harp/protocol/_message_type.py (100%) rename src/{harp-protocol => packages/harp-protocol/src}/harp/protocol/_payload.py (100%) rename src/{harp-protocol => packages/harp-protocol/src}/harp/protocol/_payload_converters.py (100%) rename src/{harp-protocol => packages/harp-protocol/src}/harp/protocol/_payload_type.py (100%) rename src/{harp-protocol => packages/harp-protocol/src}/harp/protocol/_register.py (100%) rename src/{harp-protocol => packages/harp-protocol/src}/harp/protocol/py.typed (100%) create mode 100644 src/packages/harp-serial/pyproject.toml create mode 100644 src/packages/harp-serial/src/harp/serial/__init__.py rename src/{harp-device/src/harp/device => packages/harp-serial/src/harp/serial}/_serial.py (96%) diff --git a/docs/api/protocol.md b/docs/api/protocol.md index a222a56..2f065fd 100644 --- a/docs/api/protocol.md +++ b/docs/api/protocol.md @@ -1,4 +1,4 @@ -{% include-markdown "../../src/harp-protocol/README.md" %} +{% include-markdown "../../src/packages/harp-protocol/README.md" %} --- diff --git a/docs/api/serial.md b/docs/api/serial.md index c598ff0..e8d7bd4 100644 --- a/docs/api/serial.md +++ b/docs/api/serial.md @@ -1,4 +1,4 @@ -{% include-markdown "../../src/harp-serial/README.md" %} +{% include-markdown "../../src/packages/harp-serial/README.md" %} --- diff --git a/pyproject.toml b/pyproject.toml index d8417fb..0f91cb1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "pyharp" +name = "harp" version = "0.2.0" description = "Library for data acquisition and control of devices implementing the Harp protocol." authors = [{ name= "Hardware and Software Platform, Champalimaud Foundation", email="software@research.fchampalimaud.org"}] @@ -10,6 +10,7 @@ requires-python = ">=3.10,<4.0" dependencies = [ "harp-protocol", "harp-device", + "harp-serial", ] [project.urls] @@ -20,10 +21,11 @@ Documentation = "https://fchampalimaud.github.io/pyharp/" [tool.uv.sources] harp-protocol = { workspace = true } harp-device = { workspace = true } +harp-serial = { workspace = true } [tool.uv.workspace] members = [ - "src/*", + "src/packages/*", ] [dependency-groups] @@ -46,6 +48,7 @@ dev = [ "ty>=0.0.0", "harp-protocol", "harp-device", + "harp-serial", "typing-extensions>=4.15.0", ] @@ -71,8 +74,9 @@ extra-paths = ["tests"] [tool.ty.src] include = [ - "src/harp-protocol", - "src/harp-device/src", + "src/packages/harp-protocol/src", + "src/packages/harp-device/src", + "src/packages/harp-serial/src", ] exclude = [ "tests", diff --git a/scripts/core_registers_example.py b/scripts/core_registers_example.py index 4f2eb91..3423a5d 100644 --- a/scripts/core_registers_example.py +++ b/scripts/core_registers_example.py @@ -13,26 +13,24 @@ EnableFlag, OperationMode, # Payload classes - ClockConfigPayload, + ClockConfigurationPayload, OperationControlPayload, ResetDevicePayload, # Registers AssemblyVersion, - ClockConfig, - CoreVersionH, - CoreVersionL, + ClockConfiguration, + CoreVersionHigh, + CoreVersionLow, DeviceName, - FirmwareVersionH, - FirmwareVersionL, - Heartbeat, - HwVersionH, - HwVersionL, + FirmwareVersionHigh, + FirmwareVersionLow, + HardwareVersionHigh, + HardwareVersionLow, OperationControl, ResetDevice, SerialNumber, - TimestampMicro, - TimestampOffset, - TimestampSecond, + TimestampMicroseconds, + TimestampSeconds, WhoAmI, ) @@ -40,31 +38,29 @@ examples = [ (WhoAmI, np.uint16(1216)), - (TimestampSecond, np.uint32(3600)), - (TimestampMicro, np.uint16(500)), + (TimestampSeconds, np.uint32(3600)), + (TimestampMicroseconds, np.uint16(500)), ( OperationControl, OperationControlPayload( - operation_mode=OperationMode.Active, + operation_mode=OperationMode.ACTIVE, dump_registers=True, - visual_indicators=EnableFlag.Enabled, - operation_led=EnableFlag.Enabled, - heartbeat=EnableFlag.Enabled, + visual_indicators=EnableFlag.ENABLED, + operation_led=EnableFlag.ENABLED, + heartbeat=EnableFlag.ENABLED, ), ), (ResetDevice, ResetDevicePayload(restore_default=True, restore_name=True)), (DeviceName, "my-harp-device"), - (ClockConfig, ClockConfigPayload(clock_repeater=True, clock_unlock=True)), - (Heartbeat, np.uint16(1)), - (HwVersionH, np.uint8(2)), - (HwVersionL, np.uint8(0)), + (ClockConfiguration, ClockConfigurationPayload(clock_repeater=True, clock_unlock=True)), + (HardwareVersionHigh, np.uint8(2)), + (HardwareVersionLow, np.uint8(0)), (AssemblyVersion, np.uint8(3)), - (CoreVersionH, np.uint8(1)), - (CoreVersionL, np.uint8(4)), - (FirmwareVersionH, np.uint8(2)), - (FirmwareVersionL, np.uint8(1)), + (CoreVersionHigh, np.uint8(1)), + (CoreVersionLow, np.uint8(4)), + (FirmwareVersionHigh, np.uint8(2)), + (FirmwareVersionLow, np.uint8(1)), (SerialNumber, np.uint16(42)), - (TimestampOffset, np.uint8(0)), ] print("=== Live round-trip (format → parse) ===") @@ -78,14 +74,14 @@ # --------------------------------------------------------------------------- print("\n=== Bitfield scalar access ===") ctrl = OperationControlPayload( - operation_mode=OperationMode.Active, - heartbeat=EnableFlag.Enabled, - visual_indicators=EnableFlag.Enabled, + operation_mode=OperationMode.ACTIVE, + heartbeat=EnableFlag.ENABLED, + visual_indicators=EnableFlag.ENABLED, ) -print(f" operation_mode : {ctrl.operation_mode}") # OperationMode.Active -print(f" heartbeat : {ctrl.heartbeat}") # EnableFlag.Enabled +print(f" operation_mode : {ctrl.operation_mode}") # OperationMode.ACTIVE +print(f" heartbeat : {ctrl.heartbeat}") # EnableFlag.ENABLED print(f" dump_registers : {ctrl.dump_registers}") # False -print(f" visual_indicators : {ctrl.visual_indicators}") # EnableFlag.Enabled +print(f" visual_indicators : {ctrl.visual_indicators}") # EnableFlag.ENABLED # --------------------------------------------------------------------------- # Bulk read from a .bin file (zero-copy, vectorised) diff --git a/scripts/device_integration.py b/scripts/device_integration.py index dc519d9..94a6942 100644 --- a/scripts/device_integration.py +++ b/scripts/device_integration.py @@ -3,43 +3,41 @@ import numpy as np from harp.device import ( AssemblyVersion, - ClockConfig, - CoreVersionH, - CoreVersionL, + ClockConfiguration, + CoreVersionHigh, + CoreVersionLow, Device, - FirmwareVersionH, - FirmwareVersionL, - Heartbeat, - HwVersionH, - HwVersionL, + FirmwareVersionHigh, + FirmwareVersionLow, + HardwareVersionHigh, + HardwareVersionLow, OperationControl, ResetDevice, SerialNumber, - TimestampMicro, - TimestampSecond, + TimestampMicroseconds, + TimestampSeconds, WhoAmI, - open_serial_device, ) +from harp.serial import open_serial_device PORT = "COM4" N_READS = 10_000 CORE_REGISTERS = [ ("WhoAmI", WhoAmI), - ("HwVersionH", HwVersionH), - ("HwVersionL", HwVersionL), + ("HardwareVersionHigh", HardwareVersionHigh), + ("HardwareVersionLow", HardwareVersionLow), ("AssemblyVersion", AssemblyVersion), - ("CoreVersionH", CoreVersionH), - ("CoreVersionL", CoreVersionL), - ("FirmwareVersionH", FirmwareVersionH), - ("FirmwareVersionL", FirmwareVersionL), - ("TimestampSecond", TimestampSecond), - ("TimestampMicro", TimestampMicro), + ("CoreVersionHigh", CoreVersionHigh), + ("CoreVersionLow", CoreVersionLow), + ("FirmwareVersionHigh", FirmwareVersionHigh), + ("FirmwareVersionLow", FirmwareVersionLow), + ("TimestampSeconds", TimestampSeconds), + ("TimestampMicroseconds", TimestampMicroseconds), ("OperationControl", OperationControl), ("ResetDevice", ResetDevice), ("SerialNumber", SerialNumber), - ("ClockConfig", ClockConfig), - ("Heartbeat", Heartbeat), + ("ClockConfiguration", ClockConfiguration), ] @@ -97,5 +95,7 @@ def latency_benchmark(dev: Device, n: int = N_READS) -> None: if __name__ == "__main__": # Live device reads (requires hardware) with open_serial_device(Device, port=PORT) as dev: + sn_message = dev.read(SerialNumber) + sn = sn_message.parsed read_core_registers(dev) latency_benchmark(dev) diff --git a/src/harp-device/pyproject.toml b/src/packages/harp-device/pyproject.toml similarity index 74% rename from src/harp-device/pyproject.toml rename to src/packages/harp-device/pyproject.toml index 58d5a62..608854e 100644 --- a/src/harp-device/pyproject.toml +++ b/src/packages/harp-device/pyproject.toml @@ -1,11 +1,10 @@ [project] name = "harp-device" version = "0.1.0" -description = "Harp device interfaces — serial and other transports" +description = "Transport-agnostic Harp device protocol layer" requires-python = ">=3.11" dependencies = [ "harp-protocol", - "pyserial>=3.5", ] [build-system] diff --git a/src/harp-device/src/harp/device/__init__.py b/src/packages/harp-device/src/harp/device/__init__.py similarity index 53% rename from src/harp-device/src/harp/device/__init__.py rename to src/packages/harp-device/src/harp/device/__init__.py index 892a838..f7c5c7e 100644 --- a/src/harp-device/src/harp/device/__init__.py +++ b/src/packages/harp-device/src/harp/device/__init__.py @@ -2,30 +2,29 @@ from ._framer import HarpFramer from ._registers import ( AssemblyVersion, - ClockConfig, - ClockConfigPayload, - CoreVersionH, - CoreVersionL, + ClockConfiguration, + ClockConfigurationFlags, + ClockConfigurationPayload, + CoreVersionHigh, + CoreVersionLow, DeviceName, DeviceNamePayload, EnableFlag, - FirmwareVersionH, - FirmwareVersionL, - Heartbeat, - HwVersionH, - HwVersionL, + FirmwareVersionHigh, + FirmwareVersionLow, + HardwareVersionHigh, + HardwareVersionLow, OperationControl, OperationControlPayload, OperationMode, ResetDevice, ResetDevicePayload, + ResetFlags, SerialNumber, - TimestampMicro, - TimestampOffset, - TimestampSecond, + TimestampMicroseconds, + TimestampSeconds, WhoAmI, ) -from ._serial import SerialTransport, open_serial_device from ._transport import ITransport, TransportError __all__ = [ @@ -33,29 +32,27 @@ "HarpFramer", "ITransport", "TransportError", - "SerialTransport", - "open_serial_device", "WhoAmI", - "HwVersionH", - "HwVersionL", + "HardwareVersionHigh", + "HardwareVersionLow", "AssemblyVersion", - "CoreVersionH", - "CoreVersionL", - "FirmwareVersionH", - "FirmwareVersionL", - "TimestampSecond", - "TimestampMicro", + "CoreVersionHigh", + "CoreVersionLow", + "FirmwareVersionHigh", + "FirmwareVersionLow", + "TimestampSeconds", + "TimestampMicroseconds", "OperationControl", "OperationControlPayload", "OperationMode", "ResetDevice", "ResetDevicePayload", + "ResetFlags", "DeviceName", "DeviceNamePayload", "EnableFlag", - "ClockConfig", - "ClockConfigPayload", + "ClockConfiguration", + "ClockConfigurationFlags", + "ClockConfigurationPayload", "SerialNumber", - "TimestampOffset", - "Heartbeat", ] diff --git a/src/harp-device/src/harp/device/_device.py b/src/packages/harp-device/src/harp/device/_device.py similarity index 91% rename from src/harp-device/src/harp/device/_device.py rename to src/packages/harp-device/src/harp/device/_device.py index 92770e3..0fa3929 100644 --- a/src/harp-device/src/harp/device/_device.py +++ b/src/packages/harp-device/src/harp/device/_device.py @@ -13,21 +13,19 @@ from ._transport import ITransport, TransportError from ._registers import ( AssemblyVersion, - ClockConfig, - CoreVersionH, - CoreVersionL, + ClockConfiguration, + CoreVersionHigh, + CoreVersionLow, DeviceName, - FirmwareVersionH, - FirmwareVersionL, - Heartbeat, - HwVersionH, - HwVersionL, + FirmwareVersionHigh, + FirmwareVersionLow, + HardwareVersionHigh, + HardwareVersionLow, OperationControl, ResetDevice, SerialNumber, - TimestampMicro, - TimestampOffset, - TimestampSecond, + TimestampMicroseconds, + TimestampSeconds, WhoAmI, ) @@ -50,22 +48,20 @@ class Device: # -- exposed registers -------------------------------------------------- WhoAmI = WhoAmI - HwVersionH = HwVersionH - HwVersionL = HwVersionL + HardwareVersionHigh = HardwareVersionHigh + HardwareVersionLow = HardwareVersionLow AssemblyVersion = AssemblyVersion - CoreVersionH = CoreVersionH - CoreVersionL = CoreVersionL - FirmwareVersionH = FirmwareVersionH - FirmwareVersionL = FirmwareVersionL - TimestampSecond = TimestampSecond - TimestampMicro = TimestampMicro + CoreVersionHigh = CoreVersionHigh + CoreVersionLow = CoreVersionLow + FirmwareVersionHigh = FirmwareVersionHigh + FirmwareVersionLow = FirmwareVersionLow + TimestampSeconds = TimestampSeconds + TimestampMicroseconds = TimestampMicroseconds OperationControl = OperationControl ResetDevice = ResetDevice DeviceName = DeviceName SerialNumber = SerialNumber - TimestampOffset = TimestampOffset - ClockConfig = ClockConfig - Heartbeat = Heartbeat + ClockConfiguration = ClockConfiguration def __init__(self, transport: ITransport, *, raise_on_error: bool = True) -> None: self._transport = transport diff --git a/src/harp-device/src/harp/device/_framer.py b/src/packages/harp-device/src/harp/device/_framer.py similarity index 100% rename from src/harp-device/src/harp/device/_framer.py rename to src/packages/harp-device/src/harp/device/_framer.py diff --git a/src/harp-device/src/harp/device/_registers.py b/src/packages/harp-device/src/harp/device/_registers.py similarity index 100% rename from src/harp-device/src/harp/device/_registers.py rename to src/packages/harp-device/src/harp/device/_registers.py diff --git a/src/harp-device/src/harp/device/_transport.py b/src/packages/harp-device/src/harp/device/_transport.py similarity index 100% rename from src/harp-device/src/harp/device/_transport.py rename to src/packages/harp-device/src/harp/device/_transport.py diff --git a/src/harp-device/src/harp/device/py.typed b/src/packages/harp-device/src/harp/device/py.typed similarity index 100% rename from src/harp-device/src/harp/device/py.typed rename to src/packages/harp-device/src/harp/device/py.typed diff --git a/src/harp-protocol/README.md b/src/packages/harp-protocol/README.md similarity index 100% rename from src/harp-protocol/README.md rename to src/packages/harp-protocol/README.md diff --git a/src/harp-protocol/pyproject.toml b/src/packages/harp-protocol/pyproject.toml similarity index 96% rename from src/harp-protocol/pyproject.toml rename to src/packages/harp-protocol/pyproject.toml index b30c96c..fd340da 100644 --- a/src/harp-protocol/pyproject.toml +++ b/src/packages/harp-protocol/pyproject.toml @@ -18,4 +18,4 @@ build-backend = "uv_build" [tool.uv.build-backend] module-name = "harp.protocol" -module-root = "" +module-root = "src" diff --git a/src/harp-protocol/harp/protocol/__init__.py b/src/packages/harp-protocol/src/harp/protocol/__init__.py similarity index 100% rename from src/harp-protocol/harp/protocol/__init__.py rename to src/packages/harp-protocol/src/harp/protocol/__init__.py diff --git a/src/harp-protocol/harp/protocol/_builder.py b/src/packages/harp-protocol/src/harp/protocol/_builder.py similarity index 100% rename from src/harp-protocol/harp/protocol/_builder.py rename to src/packages/harp-protocol/src/harp/protocol/_builder.py diff --git a/src/harp-protocol/harp/protocol/_checksum.py b/src/packages/harp-protocol/src/harp/protocol/_checksum.py similarity index 100% rename from src/harp-protocol/harp/protocol/_checksum.py rename to src/packages/harp-protocol/src/harp/protocol/_checksum.py diff --git a/src/harp-protocol/harp/protocol/_constants.py b/src/packages/harp-protocol/src/harp/protocol/_constants.py similarity index 100% rename from src/harp-protocol/harp/protocol/_constants.py rename to src/packages/harp-protocol/src/harp/protocol/_constants.py diff --git a/src/harp-protocol/harp/protocol/_message.py b/src/packages/harp-protocol/src/harp/protocol/_message.py similarity index 100% rename from src/harp-protocol/harp/protocol/_message.py rename to src/packages/harp-protocol/src/harp/protocol/_message.py diff --git a/src/harp-protocol/harp/protocol/_message_type.py b/src/packages/harp-protocol/src/harp/protocol/_message_type.py similarity index 100% rename from src/harp-protocol/harp/protocol/_message_type.py rename to src/packages/harp-protocol/src/harp/protocol/_message_type.py diff --git a/src/harp-protocol/harp/protocol/_payload.py b/src/packages/harp-protocol/src/harp/protocol/_payload.py similarity index 100% rename from src/harp-protocol/harp/protocol/_payload.py rename to src/packages/harp-protocol/src/harp/protocol/_payload.py diff --git a/src/harp-protocol/harp/protocol/_payload_converters.py b/src/packages/harp-protocol/src/harp/protocol/_payload_converters.py similarity index 100% rename from src/harp-protocol/harp/protocol/_payload_converters.py rename to src/packages/harp-protocol/src/harp/protocol/_payload_converters.py diff --git a/src/harp-protocol/harp/protocol/_payload_type.py b/src/packages/harp-protocol/src/harp/protocol/_payload_type.py similarity index 100% rename from src/harp-protocol/harp/protocol/_payload_type.py rename to src/packages/harp-protocol/src/harp/protocol/_payload_type.py diff --git a/src/harp-protocol/harp/protocol/_register.py b/src/packages/harp-protocol/src/harp/protocol/_register.py similarity index 100% rename from src/harp-protocol/harp/protocol/_register.py rename to src/packages/harp-protocol/src/harp/protocol/_register.py diff --git a/src/harp-protocol/harp/protocol/py.typed b/src/packages/harp-protocol/src/harp/protocol/py.typed similarity index 100% rename from src/harp-protocol/harp/protocol/py.typed rename to src/packages/harp-protocol/src/harp/protocol/py.typed diff --git a/src/packages/harp-serial/pyproject.toml b/src/packages/harp-serial/pyproject.toml new file mode 100644 index 0000000..c9d1b1d --- /dev/null +++ b/src/packages/harp-serial/pyproject.toml @@ -0,0 +1,18 @@ +[project] +name = "harp-serial" +version = "0.1.0" +description = "Serial transport for Harp devices" +requires-python = ">=3.11" +dependencies = [ + "harp-protocol", + "harp-device", + "pyserial>=3.5", +] + +[build-system] +requires = ["uv_build>=0.9.5,<0.10.0"] +build-backend = "uv_build" + +[tool.uv.build-backend] +module-name = "harp.serial" +module-root = "src" diff --git a/src/packages/harp-serial/src/harp/serial/__init__.py b/src/packages/harp-serial/src/harp/serial/__init__.py new file mode 100644 index 0000000..20877d0 --- /dev/null +++ b/src/packages/harp-serial/src/harp/serial/__init__.py @@ -0,0 +1,6 @@ +from ._serial import SerialTransport, open_serial_device + +__all__ = [ + "SerialTransport", + "open_serial_device", +] diff --git a/src/harp-device/src/harp/device/_serial.py b/src/packages/harp-serial/src/harp/serial/_serial.py similarity index 96% rename from src/harp-device/src/harp/device/_serial.py rename to src/packages/harp-serial/src/harp/serial/_serial.py index 1263d51..45d5b9a 100644 --- a/src/harp-device/src/harp/device/_serial.py +++ b/src/packages/harp-serial/src/harp/serial/_serial.py @@ -4,8 +4,7 @@ import serial -from ._device import Device -from ._transport import TransportError +from harp.device import Device, TransportError D = TypeVar("D", bound=Device) diff --git a/tests/test_device.py b/tests/test_device.py index e2dc451..4dd49e9 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -67,7 +67,7 @@ def test_bitflag_batch_returns_ndarray(): def test_groupmask_single_returns_enum(): p = _FlagPayload.from_buffer(bytes([0x02])) # bits 1-2 = 01 -> Active result = p.group - assert result == OperationMode.Active + assert result == OperationMode.ACTIVE assert isinstance(result, OperationMode) @@ -82,8 +82,8 @@ def test_groupmask_batch_returns_ndarray(): def _make_op_ctrl_byte( - mode: OperationMode = OperationMode.Standby, - heartbeat: EnableFlag = EnableFlag.Disabled, + mode: OperationMode = OperationMode.STANDBY, + heartbeat: EnableFlag = EnableFlag.DISABLED, ) -> int: val = int(mode) & 0x03 val |= (int(heartbeat) & 0x01) << 7 @@ -91,18 +91,18 @@ def _make_op_ctrl_byte( def test_op_ctrl_scalar_from_buffer(): - val = _make_op_ctrl_byte(OperationMode.Active, EnableFlag.Enabled) + val = _make_op_ctrl_byte(OperationMode.ACTIVE, EnableFlag.ENABLED) p = OperationControlPayload.from_buffer(bytes([val])) - assert p.operation_mode == OperationMode.Active - assert p.heartbeat == EnableFlag.Enabled + assert p.operation_mode == OperationMode.ACTIVE + assert p.heartbeat == EnableFlag.ENABLED assert p.dump_registers is False def test_op_ctrl_init_matches_from_buffer(): p_init = OperationControlPayload( - operation_mode=OperationMode.Active, heartbeat=EnableFlag.Enabled + operation_mode=OperationMode.ACTIVE, heartbeat=EnableFlag.ENABLED ) - val = _make_op_ctrl_byte(OperationMode.Active, EnableFlag.Enabled) + val = _make_op_ctrl_byte(OperationMode.ACTIVE, EnableFlag.ENABLED) p_buf = OperationControlPayload.from_buffer(bytes([val])) assert p_init.operation_mode == p_buf.operation_mode assert p_init.heartbeat == p_buf.heartbeat @@ -110,8 +110,8 @@ def test_op_ctrl_init_matches_from_buffer(): def test_op_ctrl_batch_descriptor_returns_ndarray(): vals = [ - _make_op_ctrl_byte(OperationMode.Active, EnableFlag.Enabled), - _make_op_ctrl_byte(OperationMode.Standby, EnableFlag.Disabled), + _make_op_ctrl_byte(OperationMode.ACTIVE, EnableFlag.ENABLED), + _make_op_ctrl_byte(OperationMode.STANDBY, EnableFlag.DISABLED), ] p = OperationControlPayload.from_buffer(bytes(vals)) assert isinstance(p.heartbeat, np.ndarray) @@ -120,8 +120,8 @@ def test_op_ctrl_batch_descriptor_returns_ndarray(): def test_op_ctrl_to_dataframe(): vals = [ - _make_op_ctrl_byte(OperationMode.Active, EnableFlag.Enabled), - _make_op_ctrl_byte(OperationMode.Standby, EnableFlag.Disabled), + _make_op_ctrl_byte(OperationMode.ACTIVE, EnableFlag.ENABLED), + _make_op_ctrl_byte(OperationMode.STANDBY, EnableFlag.DISABLED), ] p = OperationControlPayload.from_buffer(bytes(vals)) df = p.to_dataframe(decode_enums=False) @@ -130,7 +130,7 @@ def test_op_ctrl_to_dataframe(): np.testing.assert_array_equal(df["heartbeat"], [True, False]) np.testing.assert_array_equal( df["operation_mode"], - [int(OperationMode.Active), int(OperationMode.Standby)], + [int(OperationMode.ACTIVE), int(OperationMode.STANDBY)], ) @@ -204,20 +204,20 @@ def test_read_frames_payload_type(): def test_read_frames_bitfield_batch(): vals = [ - _make_op_ctrl_byte(OperationMode.Active, EnableFlag.Enabled), - _make_op_ctrl_byte(OperationMode.Standby, EnableFlag.Disabled), + _make_op_ctrl_byte(OperationMode.ACTIVE, EnableFlag.ENABLED), + _make_op_ctrl_byte(OperationMode.STANDBY, EnableFlag.DISABLED), ] raw = _make_frames(vals) _, payload = _read_frames(raw) np.testing.assert_array_equal(payload.heartbeat, [True, False]) np.testing.assert_array_equal( payload.operation_mode, - [int(OperationMode.Active), int(OperationMode.Standby)], + [int(OperationMode.ACTIVE), int(OperationMode.STANDBY)], ) def test_read_frames_to_dataframe(): - vals = [_make_op_ctrl_byte(OperationMode.Active), _make_op_ctrl_byte(), _make_op_ctrl_byte()] + vals = [_make_op_ctrl_byte(OperationMode.ACTIVE), _make_op_ctrl_byte(), _make_op_ctrl_byte()] raw = _make_frames(vals) _, payload = _read_frames(raw) df = payload.to_dataframe() diff --git a/uv.lock b/uv.lock index 4d72262..041f33d 100644 --- a/uv.lock +++ b/uv.lock @@ -12,9 +12,10 @@ resolution-markers = [ [manifest] members = [ + "harp", "harp-device", "harp-protocol", - "pyharp", + "harp-serial", ] [[package]] @@ -343,24 +344,83 @@ wheels = [ ] [[package]] -name = "harp-device" -version = "0.1.0" -source = { editable = "src/harp-device" } +name = "harp" +version = "0.2.0" +source = { virtual = "." } dependencies = [ + { name = "harp-device" }, { name = "harp-protocol" }, - { name = "pyserial" }, + { name = "harp-serial" }, +] + +[package.dev-dependencies] +dev = [ + { name = "harp-device" }, + { name = "harp-protocol" }, + { name = "harp-serial" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, + { name = "ty" }, + { name = "typing-extensions" }, +] +docs = [ + { name = "griffe-fieldz" }, + { name = "mkdocs" }, + { name = "mkdocs-codeinclude-plugin" }, + { name = "mkdocs-git-authors-plugin" }, + { name = "mkdocs-git-committers-plugin-2" }, + { name = "mkdocs-include-markdown-plugin" }, + { name = "mkdocs-material" }, + { name = "mkdocs-monorepo-plugin" }, + { name = "mkdocstrings-python" }, ] [package.metadata] requires-dist = [ - { name = "harp-protocol", editable = "src/harp-protocol" }, - { name = "pyserial", specifier = ">=3.5" }, + { name = "harp-device", editable = "src/packages/harp-device" }, + { name = "harp-protocol", editable = "src/packages/harp-protocol" }, + { name = "harp-serial", editable = "src/packages/harp-serial" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "harp-device", editable = "src/packages/harp-device" }, + { name = "harp-protocol", editable = "src/packages/harp-protocol" }, + { name = "harp-serial", editable = "src/packages/harp-serial" }, + { name = "pytest", specifier = ">=8.3.5" }, + { name = "pytest-cov", specifier = ">=6.1.1" }, + { name = "ruff", specifier = ">=0.11.0" }, + { name = "ty", specifier = ">=0.0.0" }, + { name = "typing-extensions", specifier = ">=4.15.0" }, +] +docs = [ + { name = "griffe-fieldz", specifier = ">=0.2.1" }, + { name = "mkdocs", specifier = ">=1.6.1" }, + { name = "mkdocs-codeinclude-plugin", specifier = ">=0.2.1" }, + { name = "mkdocs-git-authors-plugin", specifier = ">=0.9.4" }, + { name = "mkdocs-git-committers-plugin-2", specifier = ">=2.5.0" }, + { name = "mkdocs-include-markdown-plugin", specifier = ">=7.1.6" }, + { name = "mkdocs-material", specifier = ">=9.6.9" }, + { name = "mkdocs-monorepo-plugin", specifier = ">=1.1.2" }, + { name = "mkdocstrings-python", specifier = ">=1.16.6" }, ] +[[package]] +name = "harp-device" +version = "0.1.0" +source = { editable = "src/packages/harp-device" } +dependencies = [ + { name = "harp-protocol" }, +] + +[package.metadata] +requires-dist = [{ name = "harp-protocol", editable = "src/packages/harp-protocol" }] + [[package]] name = "harp-protocol" version = "0.4.0" -source = { editable = "src/harp-protocol" } +source = { editable = "src/packages/harp-protocol" } dependencies = [ { name = "numpy" }, { name = "pandas" }, @@ -374,6 +434,23 @@ requires-dist = [ { name = "typing-extensions", specifier = ">=4.0" }, ] +[[package]] +name = "harp-serial" +version = "0.1.0" +source = { editable = "src/packages/harp-serial" } +dependencies = [ + { name = "harp-device" }, + { name = "harp-protocol" }, + { name = "pyserial" }, +] + +[package.metadata] +requires-dist = [ + { name = "harp-device", editable = "src/packages/harp-device" }, + { name = "harp-protocol", editable = "src/packages/harp-protocol" }, + { name = "pyserial", specifier = ">=3.5" }, +] + [[package]] name = "idna" version = "3.13" @@ -868,65 +945,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] -[[package]] -name = "pyharp" -version = "0.2.0" -source = { virtual = "." } -dependencies = [ - { name = "harp-device" }, - { name = "harp-protocol" }, -] - -[package.dev-dependencies] -dev = [ - { name = "harp-device" }, - { name = "harp-protocol" }, - { name = "pytest" }, - { name = "pytest-cov" }, - { name = "ruff" }, - { name = "ty" }, - { name = "typing-extensions" }, -] -docs = [ - { name = "griffe-fieldz" }, - { name = "mkdocs" }, - { name = "mkdocs-codeinclude-plugin" }, - { name = "mkdocs-git-authors-plugin" }, - { name = "mkdocs-git-committers-plugin-2" }, - { name = "mkdocs-include-markdown-plugin" }, - { name = "mkdocs-material" }, - { name = "mkdocs-monorepo-plugin" }, - { name = "mkdocstrings-python" }, -] - -[package.metadata] -requires-dist = [ - { name = "harp-device", editable = "src/harp-device" }, - { name = "harp-protocol", editable = "src/harp-protocol" }, -] - -[package.metadata.requires-dev] -dev = [ - { name = "harp-device", editable = "src/harp-device" }, - { name = "harp-protocol", editable = "src/harp-protocol" }, - { name = "pytest", specifier = ">=8.3.5" }, - { name = "pytest-cov", specifier = ">=6.1.1" }, - { name = "ruff", specifier = ">=0.11.0" }, - { name = "ty", specifier = ">=0.0.0" }, - { name = "typing-extensions", specifier = ">=4.15.0" }, -] -docs = [ - { name = "griffe-fieldz", specifier = ">=0.2.1" }, - { name = "mkdocs", specifier = ">=1.6.1" }, - { name = "mkdocs-codeinclude-plugin", specifier = ">=0.2.1" }, - { name = "mkdocs-git-authors-plugin", specifier = ">=0.9.4" }, - { name = "mkdocs-git-committers-plugin-2", specifier = ">=2.5.0" }, - { name = "mkdocs-include-markdown-plugin", specifier = ">=7.1.6" }, - { name = "mkdocs-material", specifier = ">=9.6.9" }, - { name = "mkdocs-monorepo-plugin", specifier = ">=1.1.2" }, - { name = "mkdocstrings-python", specifier = ">=1.16.6" }, -] - [[package]] name = "pymdown-extensions" version = "10.21.2" From fd9780e86f78c85cb9b7e9a7415cba677403d832 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Thu, 25 Jun 2026 02:53:08 -0700 Subject: [PATCH 239/267] Add register map constant to device package --- .../harp-device/src/harp/device/__init__.py | 2 + .../harp-device/src/harp/device/_device.py | 31 ------------ .../src/harp/device/_register_map.py | 48 +++++++++++++++++++ 3 files changed, 50 insertions(+), 31 deletions(-) create mode 100644 src/packages/harp-device/src/harp/device/_register_map.py diff --git a/src/packages/harp-device/src/harp/device/__init__.py b/src/packages/harp-device/src/harp/device/__init__.py index f7c5c7e..e61d1f3 100644 --- a/src/packages/harp-device/src/harp/device/__init__.py +++ b/src/packages/harp-device/src/harp/device/__init__.py @@ -25,6 +25,7 @@ TimestampSeconds, WhoAmI, ) +from ._register_map import REGISTER_MAP from ._transport import ITransport, TransportError __all__ = [ @@ -32,6 +33,7 @@ "HarpFramer", "ITransport", "TransportError", + "REGISTER_MAP", "WhoAmI", "HardwareVersionHigh", "HardwareVersionLow", diff --git a/src/packages/harp-device/src/harp/device/_device.py b/src/packages/harp-device/src/harp/device/_device.py index 0fa3929..09a5382 100644 --- a/src/packages/harp-device/src/harp/device/_device.py +++ b/src/packages/harp-device/src/harp/device/_device.py @@ -12,20 +12,6 @@ from ._framer import HarpFramer from ._transport import ITransport, TransportError from ._registers import ( - AssemblyVersion, - ClockConfiguration, - CoreVersionHigh, - CoreVersionLow, - DeviceName, - FirmwareVersionHigh, - FirmwareVersionLow, - HardwareVersionHigh, - HardwareVersionLow, - OperationControl, - ResetDevice, - SerialNumber, - TimestampMicroseconds, - TimestampSeconds, WhoAmI, ) @@ -46,23 +32,6 @@ class Device: #: Expected ``WhoAmI`` of the device this class models; ``0x0`` skips the check. __whoami__: ClassVar[int] = 0x0 - # -- exposed registers -------------------------------------------------- - WhoAmI = WhoAmI - HardwareVersionHigh = HardwareVersionHigh - HardwareVersionLow = HardwareVersionLow - AssemblyVersion = AssemblyVersion - CoreVersionHigh = CoreVersionHigh - CoreVersionLow = CoreVersionLow - FirmwareVersionHigh = FirmwareVersionHigh - FirmwareVersionLow = FirmwareVersionLow - TimestampSeconds = TimestampSeconds - TimestampMicroseconds = TimestampMicroseconds - OperationControl = OperationControl - ResetDevice = ResetDevice - DeviceName = DeviceName - SerialNumber = SerialNumber - ClockConfiguration = ClockConfiguration - def __init__(self, transport: ITransport, *, raise_on_error: bool = True) -> None: self._transport = transport self.raise_on_error = raise_on_error diff --git a/src/packages/harp-device/src/harp/device/_register_map.py b/src/packages/harp-device/src/harp/device/_register_map.py new file mode 100644 index 0000000..bc532dc --- /dev/null +++ b/src/packages/harp-device/src/harp/device/_register_map.py @@ -0,0 +1,48 @@ +"""Address → register-class map for the core Harp registers. + +Downstream device packages spread this into their own map:: + + from harp.device import REGISTER_MAP as _CORE_REGISTER_MAP + + REGISTER_MAP = {**_CORE_REGISTER_MAP, 32: DigitalInputState, ...} +""" + +from typing import Any + +from harp.protocol import RegisterBase + +from ._registers import ( + AssemblyVersion, + ClockConfiguration, + CoreVersionHigh, + CoreVersionLow, + DeviceName, + FirmwareVersionHigh, + FirmwareVersionLow, + HardwareVersionHigh, + HardwareVersionLow, + OperationControl, + ResetDevice, + SerialNumber, + TimestampMicroseconds, + TimestampSeconds, + WhoAmI, +) + +REGISTER_MAP: dict[int, type[RegisterBase[Any]]] = { + 0: WhoAmI, + 1: HardwareVersionHigh, + 2: HardwareVersionLow, + 3: AssemblyVersion, + 4: CoreVersionHigh, + 5: CoreVersionLow, + 6: FirmwareVersionHigh, + 7: FirmwareVersionLow, + 8: TimestampSeconds, + 9: TimestampMicroseconds, + 10: OperationControl, + 11: ResetDevice, + 12: DeviceName, + 13: SerialNumber, + 14: ClockConfiguration, +} From 0f0365a3fb1ebd68c4f3a8c421375bfce22aeb37 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Thu, 25 Jun 2026 03:32:34 -0700 Subject: [PATCH 240/267] Add data package --- docs/examples/get_info/get_info.py | 24 +++--- docs/examples/index.md | 3 +- .../read_and_write_from_registers.md | 7 +- .../read_and_write_from_registers.py | 38 +++------ .../read_data_to_dataframe.md | 11 +++ .../read_data_to_dataframe.py | 11 +++ mkdocs.yml | 1 + pyproject.toml | 4 + scripts/benchmark_analog_data.py | 9 ++- scripts/core_registers_example.py | 3 +- src/packages/harp-data/README.md | 32 ++++++++ src/packages/harp-data/pyproject.toml | 18 +++++ .../harp-data/src/harp/data/__init__.py | 7 ++ .../harp-data/src/harp/data/_reader.py | 77 +++++++++++++++++++ src/packages/harp-device/README.md | 35 +++++++++ src/packages/harp-protocol/README.md | 15 ++++ src/packages/harp-protocol/pyproject.toml | 1 - .../src/harp/protocol/__init__.py | 2 + .../src/harp/protocol/_payload.py | 60 +++++++++------ .../src/harp/protocol/_register.py | 33 -------- src/packages/harp-serial/README.md | 21 +++++ tests/protocol/test_converter.py | 3 +- tests/protocol/test_payload.py | 19 +++-- tests/protocol/test_register.py | 7 +- tests/protocol/test_register_modeling.py | 3 +- tests/test_device.py | 7 +- uv.lock | 24 +++++- 27 files changed, 347 insertions(+), 128 deletions(-) create mode 100644 docs/examples/read_data_to_dataframe/read_data_to_dataframe.md create mode 100644 docs/examples/read_data_to_dataframe/read_data_to_dataframe.py create mode 100644 src/packages/harp-data/README.md create mode 100644 src/packages/harp-data/pyproject.toml create mode 100644 src/packages/harp-data/src/harp/data/__init__.py create mode 100644 src/packages/harp-data/src/harp/data/_reader.py create mode 100644 src/packages/harp-device/README.md create mode 100644 src/packages/harp-serial/README.md diff --git a/docs/examples/get_info/get_info.py b/docs/examples/get_info/get_info.py index f1d4bd9..cc0c350 100755 --- a/docs/examples/get_info/get_info.py +++ b/docs/examples/get_info/get_info.py @@ -1,18 +1,14 @@ -from pyharp.protocol.device import Device +from harp.device import REGISTER_MAP, Device, WhoAmI +from harp.serial import open_serial_device SERIAL_PORT = "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port) -# Open serial connection and save communication to a file -device = Device(SERIAL_PORT, "dump.bin") +# Open a serial connection to the device (closed automatically on exit). +with open_serial_device(Device, port=SERIAL_PORT) as device: + # Identify the device. + print("WhoAmI:", device.read(WhoAmI).parsed) -# Display device's info on screen -device.info() - -# Dump device's registers -reg_dump = device.dump_registers() -for reg_reply in reg_dump: - print(reg_reply) - print() - -# Close connection -device.disconnect() + # Dump every core register. + for address, register in sorted(REGISTER_MAP.items()): + reply = device.read(register) + print(f"{register.__name__:24s} (addr {address:2d}) = {reply.parsed}") diff --git a/docs/examples/index.md b/docs/examples/index.md index 554e5ba..52e4245 100644 --- a/docs/examples/index.md +++ b/docs/examples/index.md @@ -5,4 +5,5 @@ This section contains some examples to help you get started with `harp`. Here's the complete list of available examples: - [Getting Device Info](./get_info/get_info.md) - connect to a Harp device and read its information. -- [Write and Read from Registers](./read_and_write_from_registers/read_and_write_from_registers.md) - connect to the Harp Behavior, read from a digital input and write to a digital output. +- [Read and Write from Registers](./read_and_write_from_registers/read_and_write_from_registers.md) - connect to a Harp device and read and write its registers. +- [Reading Data into a DataFrame](./read_data_to_dataframe/read_data_to_dataframe.md) - load a register's binary data file into a pandas DataFrame with `harp.data`. diff --git a/docs/examples/read_and_write_from_registers/read_and_write_from_registers.md b/docs/examples/read_and_write_from_registers/read_and_write_from_registers.md index 040f6e4..d67f118 100644 --- a/docs/examples/read_and_write_from_registers/read_and_write_from_registers.md +++ b/docs/examples/read_and_write_from_registers/read_and_write_from_registers.md @@ -1,6 +1,6 @@ # Read and Write from Registers -This example demonstrates how to read and write from registers. In this particular example, the [Harp Behavior](https://harp-tech.org/api/Harp.Behavior.html) is used to read from the DI3 pin and to turn on and off the DO0 pin, according to the schematics shown [below](#schematics). +This example demonstrates how to read and write from registers, using the core registers exposed by `harp.device`. Device-specific registers (e.g. a [Harp Behavior](https://harp-tech.org/api/Harp.Behavior.html)'s digital I/O) are used the same way — pass that device's register classes to `read`/`write`. !!! warning Don't forget to change the `SERIAL_PORT` to the one that corresponds to your device! The `SERIAL_PORT` must be denoted as `/dev/ttyUSBx` in Linux and `COMx` in Windows, where `x` is the number of the serial port. @@ -10,8 +10,3 @@ This example demonstrates how to read and write from registers. In this particul [](./read_and_write_from_registers.py) ``` - -## Schematics - -!!! warning - _TODO_ diff --git a/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py b/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py index d38e86c..10371d3 100755 --- a/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py +++ b/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py @@ -1,31 +1,17 @@ -from serial import SerialException - -from harp.protocol import MessageType, PayloadType -from harp.protocol.messages import HarpMessage -from harp.serial.device import Device +from harp.device import Device, OperationControl, OperationControlPayload, OperationMode, WhoAmI +from harp.serial import open_serial_device SERIAL_PORT = "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port) -# Open serial connection and save communication to a file -device = Device(SERIAL_PORT, "dump.bin") - -# Check if the device is a Harp Behavior -if not device.WHO_AM_I == 1216: - raise SerialException("This is not a Harp Behavior.") - -# Read initial DI3 state -reply = device.send(HarpMessage(MessageType.READ, PayloadType.U8, 32)) -print(reply.payload & 0x08) - -# Turn DO0 on and read DI3 state after it -reply = device.send(HarpMessage(MessageType.READ, PayloadType.U8, 34, 0x400)) -reply = device.send(HarpMessage(MessageType.READ, PayloadType.U8, 32)) -print(reply.payload & 0x08) +with open_serial_device(Device, port=SERIAL_PORT) as device: + # Read a scalar register. + print("WhoAmI:", device.read(WhoAmI).parsed) -# Turn DO0 off and read DI3 state again -reply = device.send(HarpMessage(MessageType.READ, PayloadType.U8, 35, 0x400)) -reply = device.send(HarpMessage(MessageType.READ, PayloadType.U8, 32)) -print(reply.payload & 0x08) + # Read a structured register and inspect a field. + control = device.read(OperationControl).parsed + print("operation_mode before:", control.operation_mode) -# Close connection -device.disconnect() + # Write the register, then read it back to confirm the change. + device.write(OperationControl, OperationControlPayload(operation_mode=OperationMode.ACTIVE)) + control = device.read(OperationControl).parsed + print("operation_mode after:", control.operation_mode) diff --git a/docs/examples/read_data_to_dataframe/read_data_to_dataframe.md b/docs/examples/read_data_to_dataframe/read_data_to_dataframe.md new file mode 100644 index 0000000..9162774 --- /dev/null +++ b/docs/examples/read_data_to_dataframe/read_data_to_dataframe.md @@ -0,0 +1,11 @@ +# Reading Data into a DataFrame + +This example demonstrates how to load a Harp register's binary data file into a +pandas DataFrame using `harp.data`. The register definition tells `read_dataframe` +how to decode each frame, so you get named columns (and decoded enums) for free. + + +```python +[](./read_data_to_dataframe.py) +``` + diff --git a/docs/examples/read_data_to_dataframe/read_data_to_dataframe.py b/docs/examples/read_data_to_dataframe/read_data_to_dataframe.py new file mode 100644 index 0000000..93a56af --- /dev/null +++ b/docs/examples/read_data_to_dataframe/read_data_to_dataframe.py @@ -0,0 +1,11 @@ +from harp.data import read_dataframe +from harp.device import OperationControl + +# Parse a register's binary dump into a pandas DataFrame — one row per frame, +# one column per field, plus a leading "timestamp" column. +df = read_dataframe(OperationControl, "OperationControl.bin", timestamp=True) +print(df.head()) + +# `read_dataframe` also accepts raw bytes or an open binary file object: +with open("OperationControl.bin", "rb") as f: + df = read_dataframe(OperationControl, f) diff --git a/mkdocs.yml b/mkdocs.yml index 8b34537..7143242 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -79,6 +79,7 @@ nav: - examples/index.md - Getting Device Info: examples/get_info/get_info.md - Read and Write from Registers: examples/read_and_write_from_registers/read_and_write_from_registers.md + - Reading Data into a DataFrame: examples/read_data_to_dataframe/read_data_to_dataframe.md - Devices: '*include ./harp.devices/*/docs/examples.mkdocs.yml' - API: - Devices: '*include ./harp.devices/*/mkdocs.yml' diff --git a/pyproject.toml b/pyproject.toml index 0f91cb1..046c529 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ dependencies = [ "harp-protocol", "harp-device", "harp-serial", + "harp-data", ] [project.urls] @@ -22,6 +23,7 @@ Documentation = "https://fchampalimaud.github.io/pyharp/" harp-protocol = { workspace = true } harp-device = { workspace = true } harp-serial = { workspace = true } +harp-data = { workspace = true } [tool.uv.workspace] members = [ @@ -49,6 +51,7 @@ dev = [ "harp-protocol", "harp-device", "harp-serial", + "harp-data", "typing-extensions>=4.15.0", ] @@ -77,6 +80,7 @@ include = [ "src/packages/harp-protocol/src", "src/packages/harp-device/src", "src/packages/harp-serial/src", + "src/packages/harp-data/src", ] exclude = [ "tests", diff --git a/scripts/benchmark_analog_data.py b/scripts/benchmark_analog_data.py index f1a4eb8..06d1450 100644 --- a/scripts/benchmark_analog_data.py +++ b/scripts/benchmark_analog_data.py @@ -10,8 +10,8 @@ harp_io.read(file, columns=[...]) → DataFrame directly pyharp strategy: - AnalogData.read_frames(file) → (timestamps, payload) - payload.to_dataframe() → DataFrame with named columns + AnalogData.parse_bulk(file) → (..., timestamps, ..., payload) + harp.data.to_dataframe(payload) → DataFrame with named columns Both are zero-copy for the payload bytes (strided np.ndarray views into the raw file buffer). The harp-python path builds the DataFrame in one shot; the pyharp @@ -31,6 +31,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent)) from scripts._harp_io import read as harp_read # vendored harp-python read() +from harp.data import read_dataframe, to_dataframe from harp.protocol import PayloadBase, Field, PayloadType, RegisterBase, IdentityConverter @@ -70,7 +71,7 @@ def pyharp_read(path: Path, *, include_timestamp: bool = True): """pyharp path: parse_bulk → to_dataframe.""" bytes_2_parse = path.read_bytes() _data, timestamps, msg_type, payload = AnalogData.parse_bulk(bytes_2_parse) - df = payload.to_dataframe() + df = to_dataframe(payload) if include_timestamp: df.insert(0, "timestamp", timestamps) return df @@ -78,7 +79,7 @@ def pyharp_read(path: Path, *, include_timestamp: bool = True): def pyharp_read_dataframe(path: Path, *, timestamp: bool = True): """pyharp one-call path: read_dataframe.""" - return AnalogData.read_dataframe(path.read_bytes(), timestamp=timestamp) + return read_dataframe(AnalogData, path.read_bytes(), timestamp=timestamp) def harp_python_read(path: Path): diff --git a/scripts/core_registers_example.py b/scripts/core_registers_example.py index 3423a5d..24591a8 100644 --- a/scripts/core_registers_example.py +++ b/scripts/core_registers_example.py @@ -34,6 +34,7 @@ WhoAmI, ) +from harp.data import to_dataframe from harp.protocol import HarpMessage examples = [ @@ -97,6 +98,6 @@ print(f" heartbeat : {payload.heartbeat}") print("\n DataFrame:") -df = payload.to_dataframe() +df = to_dataframe(payload) df.insert(0, "timestamp", timestamps) print(df.to_string(index=False)) diff --git a/src/packages/harp-data/README.md b/src/packages/harp-data/README.md new file mode 100644 index 0000000..d489be0 --- /dev/null +++ b/src/packages/harp-data/README.md @@ -0,0 +1,32 @@ +# harp-data + +Load Harp register data into pandas DataFrames. This is the package that pulls +in `pandas` — [`harp-protocol`](../harp-protocol) stays numpy-only and exposes a +pandas-free `ColumnData` view that this package assembles into a DataFrame. + +## Read a register from a file + +`read_dataframe` takes a register and a source (path, bytes, or open binary +file) and returns one row per frame: + +```python +from harp.data import read_dataframe +from my_device import AnalogData + +df = read_dataframe(AnalogData, "AnalogData.bin") +df = read_dataframe(AnalogData, raw, timestamp=True, message_type=False, decode_enums=True) +``` + +Enum fields decode to `pd.Categorical` (`decode_enums=False` keeps raw codes). + +## From an already-parsed payload + +If you already have a batched payload (e.g. from `register.parse_bulk`), convert +it directly: + +```python +from harp.data import to_dataframe + +_data, timestamps, _msg, payload = AnalogData.parse_bulk(raw) +df = to_dataframe(payload) +``` diff --git a/src/packages/harp-data/pyproject.toml b/src/packages/harp-data/pyproject.toml new file mode 100644 index 0000000..d7e34d5 --- /dev/null +++ b/src/packages/harp-data/pyproject.toml @@ -0,0 +1,18 @@ +[project] +name = "harp-data" +version = "0.1.0" +description = "Load Harp device data into pandas DataFrames" +requires-python = ">=3.11" +dependencies = [ + "harp-protocol", + "numpy>=1.24", + "pandas>=2.0", +] + +[build-system] +requires = ["uv_build>=0.9.5,<0.10.0"] +build-backend = "uv_build" + +[tool.uv.build-backend] +module-name = "harp.data" +module-root = "src" diff --git a/src/packages/harp-data/src/harp/data/__init__.py b/src/packages/harp-data/src/harp/data/__init__.py new file mode 100644 index 0000000..9aa9826 --- /dev/null +++ b/src/packages/harp-data/src/harp/data/__init__.py @@ -0,0 +1,7 @@ +from ._reader import columns_to_dataframe, read_dataframe, to_dataframe + +__all__ = [ + "read_dataframe", + "to_dataframe", + "columns_to_dataframe", +] diff --git a/src/packages/harp-data/src/harp/data/_reader.py b/src/packages/harp-data/src/harp/data/_reader.py new file mode 100644 index 0000000..b015c18 --- /dev/null +++ b/src/packages/harp-data/src/harp/data/_reader.py @@ -0,0 +1,77 @@ +"""Load Harp register data into pandas DataFrames.""" + +from pathlib import Path +from typing import Any, BinaryIO, Union + +import numpy as np +import pandas as pd +from harp.protocol import Column, RegisterBase + +Source = Union[str, Path, bytes, bytearray, memoryview, BinaryIO] + +_MSG_NAMES = np.array(["_NONE", "Read", "Write", "Event"]) + + +def _read_bytes(source: Source) -> bytes: + if isinstance(source, (bytes, bytearray, memoryview)): + return bytes(source) + if hasattr(source, "read"): # open binary file / stream + return source.read() + return Path(source).read_bytes() + + +def columns_to_dataframe(columns: list[Column]) -> pd.DataFrame: + """Assemble :class:`~harp.protocol.Column` objects into a DataFrame. + + Enum-backed columns (``categories`` set) become ``pd.Categorical`` built + from codes — no string materialization; everything else is used as-is. + """ + return pd.DataFrame( + { + c.name: ( + pd.Categorical.from_codes(c.data, categories=c.categories) + if c.categories is not None + else c.data + ) + for c in columns + } + ) + + +def to_dataframe(payload: Any, *, decode_enums: bool = True) -> pd.DataFrame: + """Turn a (batched) payload into a DataFrame, one row per frame.""" + return columns_to_dataframe(payload.to_columns(decode_enums=decode_enums)) + + +def read_dataframe( + register: type[RegisterBase[Any]], + source: Source, + *, + timestamp: bool = True, + message_type: bool = False, + decode_enums: bool = True, +) -> pd.DataFrame: + """Parse all frames of ``register`` from ``source`` into a DataFrame. + + ``source`` may be a file path, raw bytes, or an open binary file object. + ``timestamp`` and ``message_type`` insert leading columns; ``decode_enums`` + controls whether enum fields become ``pd.Categorical`` (True) or raw codes. + """ + raw = _read_bytes(source) + _data, timestamps, msg_view, payload = register.parse_bulk(raw, parse_timestamp=timestamp) + df = to_dataframe(payload, decode_enums=decode_enums) + + if message_type and msg_view is not None: + df.insert( + 0, + "message_type", + pd.Categorical(_MSG_NAMES[msg_view & 0x03], categories=_MSG_NAMES[1:]), + ) + if timestamp: + if timestamps is None: + raise ValueError( + "Buffer contains no timestamp data; pass timestamp=False to suppress " + "the timestamp column." + ) + df.insert(0, "timestamp", timestamps) + return df diff --git a/src/packages/harp-device/README.md b/src/packages/harp-device/README.md new file mode 100644 index 0000000..f4b0f01 --- /dev/null +++ b/src/packages/harp-device/README.md @@ -0,0 +1,35 @@ +# harp-device + +The transport-agnostic device layer for the Harp protocol: the common Harp +registers and a `Device` base that handles framing, request/reply and register +access. It depends only on [`harp-protocol`](../harp-protocol) — no transport +dependencies. Pair it with a transport (e.g. [`harp-serial`](../harp-serial)). + +## Read/write registers + +A `Device` is driven over a transport; `read`/`write` take a register class: + +```python +from harp.device import Device, WhoAmI, OperationControl + +# `device` is a Device opened over some transport (see harp-serial) +who = device.read(WhoAmI).parsed # -> np.uint16 +device.write(OperationControl, payload) # write a register +``` + +## Extending for a specific device + +Downstream (often generated) packages add their registers and spread the core +`REGISTER_MAP`, and may set `__whoami__` for identity validation on connect: + +```python +from harp.device import Device, REGISTER_MAP as _CORE_REGISTER_MAP + +class MyDevice(Device): + __whoami__ = 1216 + +REGISTER_MAP = {**_CORE_REGISTER_MAP, 32: DigitalInputState, ...} +``` + +A new transport is just an object implementing the `ITransport` protocol +(`open`/`write`/`read`/`close`). diff --git a/src/packages/harp-protocol/README.md b/src/packages/harp-protocol/README.md index 80289eb..1afee4d 100644 --- a/src/packages/harp-protocol/README.md +++ b/src/packages/harp-protocol/README.md @@ -5,3 +5,18 @@ The Harp Protocol is a binary communication protocol created in order to facilitate and unify the interaction between different devices. It was designed with efficiency and ease of parsing in mind. For more detail please check Harp Tech's official documentation [here](https://harp-tech.org/protocol/BinaryProtocol-8bit.html). + +`harp-protocol` provides the building blocks: message framing and the typed register/payload DSL. Each register knows how to build (`format`) and decode (`parse`) its frames. + +```python +import numpy as np +from harp.protocol import HarpMessage, RegisterU16 + +class WhoAmI(RegisterU16): + address = 0 + +frame = WhoAmI.format(np.uint16(1216)) # build a Write frame +value = WhoAmI.parse(HarpMessage.parse(frame)) # -> np.uint16(1216) +``` + +It carries no transport or device logic — see [`harp-device`](../harp-device) for the device layer. diff --git a/src/packages/harp-protocol/pyproject.toml b/src/packages/harp-protocol/pyproject.toml index fd340da..fd44395 100644 --- a/src/packages/harp-protocol/pyproject.toml +++ b/src/packages/harp-protocol/pyproject.toml @@ -8,7 +8,6 @@ keywords = ['python', 'harp'] requires-python = ">=3.10,<4.0" dependencies = [ "numpy>=1.24", - "pandas>=2.0", "typing-extensions>=4.0", ] diff --git a/src/packages/harp-protocol/src/harp/protocol/__init__.py b/src/packages/harp-protocol/src/harp/protocol/__init__.py index 7e4d497..8f42beb 100644 --- a/src/packages/harp-protocol/src/harp/protocol/__init__.py +++ b/src/packages/harp-protocol/src/harp/protocol/__init__.py @@ -34,6 +34,7 @@ PayloadS64Array, PayloadFloatArray, AnonymousPayload, + Column, ) from ._payload_type import PayloadType from ._register import ( @@ -79,6 +80,7 @@ "PayloadBase", "StructPayload", "AnonymousPayload", + "Column", "Field", "BitFlag", "GroupMask", diff --git a/src/packages/harp-protocol/src/harp/protocol/_payload.py b/src/packages/harp-protocol/src/harp/protocol/_payload.py index cf957f9..247508b 100644 --- a/src/packages/harp-protocol/src/harp/protocol/_payload.py +++ b/src/packages/harp-protocol/src/harp/protocol/_payload.py @@ -13,7 +13,6 @@ ) import numpy as np -import pandas as pd from numpy.typing import NDArray from typing_extensions import Self, Sentinel, dataclass_transform @@ -28,6 +27,24 @@ _DEFAULT_ELEMENT = np.dtype(np.uint8) +@dataclass(frozen=True, slots=True, eq=False) +class Column: + """One column of a batched payload. + + ``data`` is a 1-D numpy array (one row per frame). When ``categories`` is + not ``None`` the column is enum-backed: ``data`` holds integer category + *codes* and ``categories`` the ordered labels, so a consumer can map codes + to labels without copying. + + ``eq=False`` keeps identity comparison — field-wise equality would hit + numpy's ambiguous-truth-value error on the ``data`` array. + """ + + name: str + data: NDArray[Any] + categories: Any | None = None + + @dataclass(frozen=True) class _FieldSlot: """One physical numpy field: its dtype and byte offset within the record.""" @@ -339,7 +356,7 @@ def __get__(self, obj: "PayloadBase", owner: object = None) -> "NDArray[Any]": . def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: if obj is None: return self - # Enum batches return raw integer codes (see to_dataframe for Categorical decoding). + # Enum batches return raw integer codes (see to_columns for label decoding). return (obj._arr[self._slot] & self._mask) >> self._shift @@ -366,7 +383,7 @@ class Batch(Protocol[_PT]): def __len__(self) -> int: ... # type: ignore[empty-body] - def to_dataframe(self, *, decode_enums: bool = True) -> "pd.DataFrame": ... # type: ignore[empty-body] + def to_columns(self, *, decode_enums: bool = True) -> "list[Column]": ... # type: ignore[empty-body] def __getattr__(self, name: str) -> "NDArray[Any]": ... # type: ignore[empty-body] @@ -533,7 +550,7 @@ class PayloadBase(Generic[NpStructT]): # Structured numpy dtype describing the memory layout of a single payload record. dtype: ClassVar[np.dtype] - # Field names shown in __repr__ and used as DataFrame column order. + # Field names shown in __repr__ and used as the column order. _repr_fields: ClassVar[tuple[str, ...]] # The scalar twin of this class (identity for scalar classes, points to scalar from Batch). _scalar_cls: ClassVar["type[PayloadBase]"] @@ -721,38 +738,37 @@ def from_buffer(cls, buf: bytes | bytearray | memoryview) -> Self: def raw_payload(self) -> NDArray[NpStructT]: return self._arr - def to_dataframe(self, *, decode_enums: bool = True) -> pd.DataFrame: + def to_columns(self, *, decode_enums: bool = True) -> list[Column]: arr = np.atleast_1d(self._arr) cls = type(self) repr_fields = self._repr_fields + cols: list[Column] = [] has_bitfield = any(_is_masked(cls._mro_descriptor(f)) for f in repr_fields) if has_bitfield: - cols: dict[str, object] = {} for f in repr_fields: desc = cls._mro_descriptor(f) if isinstance(desc, _GROUP_MASK_TYPES): # masked enum sub-field slot_col = arr[desc._slot] # ty: ignore[invalid-argument-type] raw = (slot_col & desc._mask) >> desc._shift if decode_enums: - codes = desc._code_lookup[raw] - cols[f] = pd.Categorical.from_codes(codes, categories=desc._categories) + cols.append(Column(f, desc._code_lookup[raw], desc._categories)) else: - cols[f] = raw + cols.append(Column(f, raw)) elif ( isinstance(desc, _FIELD_TYPES) and desc._mask is not None ): # masked numeric sub-field slot_col = arr[desc._slot] # ty: ignore[invalid-argument-type] raw = (slot_col & desc._mask) >> desc._shift - cols[f] = desc._converter.decode_batch(raw.astype(desc._converter.dtype)) + decoded = desc._converter.decode_batch(raw.astype(desc._converter.dtype)) + cols.append(Column(f, decoded)) elif isinstance(desc, _BIT_FLAG_TYPES): slot_col = arr[desc._slot] # ty: ignore[invalid-argument-type] - cols[f] = (slot_col & desc._mask) != 0 + cols.append(Column(f, (slot_col & desc._mask) != 0)) else: - cols[f] = np.atleast_1d(getattr(self, f)) - return pd.DataFrame(cols) + cols.append(Column(f, np.atleast_1d(getattr(self, f)))) + return cols - cols = {} names = self.dtype.names single_value_slot = names == ("value",) for name in names: # ty: ignore[not-iterable] @@ -761,21 +777,21 @@ def to_dataframe(self, *, decode_enums: bool = True) -> pd.DataFrame: desc._converter, _IdentityConverter ) if uses_converter: - cols[name] = np.atleast_1d(getattr(self, name)) + cols.append(Column(name, np.atleast_1d(getattr(self, name)))) continue field_dtype, _ = self.dtype.fields[name] # ty: ignore[invalid-assignment, invalid-argument-type, not-subscriptable] sub = arr[name] # ty: ignore[invalid-argument-type] if field_dtype.subdtype is None: - cols[name] = sub + cols.append(Column(name, sub)) else: _, subshape = field_dtype.subdtype count = int(np.prod(subshape)) flat = sub.reshape(len(arr), count) for i in range(count): col = str(i) if single_value_slot else f"{name}_{i}" - cols[col] = flat[:, i] - return pd.DataFrame(cols) + cols.append(Column(col, flat[:, i])) + return cols def __len__(self) -> int: return 1 if self._arr.ndim == 0 else len(self._arr) @@ -922,19 +938,19 @@ def _repr_kwargs(self) -> str: def __repr__(self) -> str: return f"{type(self).__name__}({self._repr_kwargs()})" - def to_dataframe(self, *, decode_enums: bool = True) -> pd.DataFrame: + def to_columns(self, *, decode_enums: bool = True) -> list[Column]: if self._converter is not None: arr = self._arr # decode_batch wants a leading row axis; a single record lacks one # (its ndim equals the converter slot's own ndim). if arr.ndim == self._converter.dtype.ndim: arr = arr[np.newaxis, ...] - return pd.DataFrame({"value": self._converter.decode_batch(arr)}) + return [Column("value", self._converter.decode_batch(arr))] arr = np.atleast_1d(self._arr) # Sub-array dtype (array register): shape is already (N, length). if arr.ndim > 1: - return pd.DataFrame({str(i): arr[:, i] for i in range(arr.shape[1])}) - return pd.DataFrame({"value": arr}) + return [Column(str(i), arr[:, i]) for i in range(arr.shape[1])] + return [Column("value", arr)] @final diff --git a/src/packages/harp-protocol/src/harp/protocol/_register.py b/src/packages/harp-protocol/src/harp/protocol/_register.py index 258145d..a9fc9e3 100644 --- a/src/packages/harp-protocol/src/harp/protocol/_register.py +++ b/src/packages/harp-protocol/src/harp/protocol/_register.py @@ -2,7 +2,6 @@ from typing import Any, ClassVar, Generic, TypeVar, cast, final, overload import numpy as np -import pandas as pd from numpy.typing import NDArray from typing_extensions import Sentinel @@ -135,38 +134,6 @@ def parse_bulk( payload = payload_cls.from_array(payload_arr) return data, timestamps, msgtype_view, cast("Batch[Any]", payload) - @classmethod - def read_dataframe( - cls, - source: bytes | bytearray | memoryview, - *, - timestamp: bool = True, - message_type: bool = False, - decode_enums: bool = True, - ) -> "pd.DataFrame": - """Parse all frames into a DataFrame. - - ``timestamp`` and ``message_type`` insert leading columns. - ``decode_enums`` controls whether ``_GroupMask`` slots become - ``pd.Categorical`` (True) or raw integers (False). - """ - _data, timestamps, msg_view, payload = cls.parse_bulk(source, parse_timestamp=timestamp) - df = payload.to_dataframe(decode_enums=decode_enums) - if message_type and msg_view is not None: - _msg_names = np.array(["_NONE", "Read", "Write", "Event"]) - df.insert( - 0, - "message_type", - pd.Categorical(_msg_names[msg_view & 0x03], categories=_msg_names[1:]), - ) - if timestamp: - if timestamps is None: - raise ValueError( - "Buffer contains no timestamp data; pass timestamp=False to suppress the timestamp column." - ) - df.insert(0, "timestamp", timestamps) - return df - @overload @classmethod def format( diff --git a/src/packages/harp-serial/README.md b/src/packages/harp-serial/README.md new file mode 100644 index 0000000..50e74d2 --- /dev/null +++ b/src/packages/harp-serial/README.md @@ -0,0 +1,21 @@ +# harp-serial + +Serial transport for [`harp-device`](../harp-device). Provides `SerialTransport` +and the `open_serial_device` factory, which pairs a `Device` class with a serial +port. This is the package that pulls in `pyserial`. + +## Usage + +Like the builtin `open`, the returned device is connected and ready; use it in a +`with` block for guaranteed cleanup: + +```python +from harp.device import Device, WhoAmI +from harp.serial import open_serial_device + +with open_serial_device(Device, port="COM3", baudrate=1_000_000) as dev: + print(dev.read(WhoAmI).parsed) +``` + +Pass any `Device` subclass (e.g. a generated device class) instead of the base +`Device` to talk to a specific device. diff --git a/tests/protocol/test_converter.py b/tests/protocol/test_converter.py index 0f8b718..70b559f 100644 --- a/tests/protocol/test_converter.py +++ b/tests/protocol/test_converter.py @@ -12,6 +12,7 @@ import numpy as np import pytest +from harp.data import to_dataframe from harp.protocol._payload import ( PayloadBase, BitFlag, @@ -121,7 +122,7 @@ def test_string_converter_to_dataframe(): rec1 = _NamedPayload(name="hi", delta=1).raw_payload.tobytes() rec2 = _NamedPayload(name="bye", delta=2).raw_payload.tobytes() batch = _NamedPayload.from_buffer(rec1 + rec2) - df = batch.to_dataframe() + df = to_dataframe(batch) # Non-identity converter produces one column per field — no sub-array # expansion for the string field. assert list(df.columns) == ["name", "delta"] diff --git a/tests/protocol/test_payload.py b/tests/protocol/test_payload.py index 8a7fad1..e7d56b9 100644 --- a/tests/protocol/test_payload.py +++ b/tests/protocol/test_payload.py @@ -1,6 +1,7 @@ import numpy as np -import pandas as pd import pytest +from harp.data import to_dataframe +from harp.protocol import Column from harp.protocol._payload import PayloadBase, Field, _IdentityConverter @@ -12,13 +13,11 @@ class SimplePayload(PayloadBase): class BitPackedPayload(PayloadBase): packed = Field(converter=_IdentityConverter("u1")) - def to_dataframe(self, *, decode_enums: bool = True) -> pd.DataFrame: - return pd.DataFrame( - { - "flag_a": (self.raw_payload["packed"] & 0x01).astype(bool), - "flag_b": ((self.raw_payload["packed"] >> 1) & 0x01).astype(bool), - } - ) + def to_columns(self, *, decode_enums: bool = True) -> list[Column]: + return [ + Column("flag_a", (self.raw_payload["packed"] & 0x01).astype(bool)), + Column("flag_b", ((self.raw_payload["packed"] >> 1) & 0x01).astype(bool)), + ] def _make_simple_bytes(n: int) -> bytes: @@ -43,7 +42,7 @@ def test_from_buffer_values(): def test_to_dataframe_columns(): p = SimplePayload.from_buffer(_make_simple_bytes(3)) - df = p.to_dataframe() + df = to_dataframe(p) assert list(df.columns) == ["x", "y"] assert len(df) == 3 @@ -51,7 +50,7 @@ def test_to_dataframe_columns(): def test_to_dataframe_override(): arr = np.array([(0b00000011,), (0b00000001,), (0b00000010,)], dtype=BitPackedPayload.dtype) p = BitPackedPayload.from_buffer(arr.tobytes()) - df = p.to_dataframe() + df = to_dataframe(p) assert list(df.columns) == ["flag_a", "flag_b"] assert list(df["flag_a"]) == [True, True, False] assert list(df["flag_b"]) == [True, False, True] diff --git a/tests/protocol/test_register.py b/tests/protocol/test_register.py index a8f6aeb..3105501 100644 --- a/tests/protocol/test_register.py +++ b/tests/protocol/test_register.py @@ -4,6 +4,7 @@ import numpy as np import pytest +from harp.data import to_dataframe from harp.protocol._message import HarpMessage from harp.protocol._message_type import MessageType from harp.protocol._payload import ( @@ -204,7 +205,7 @@ def test_structured_register_to_dataframe(): ).tobytes() # Bulk decode goes through .Batch; from_buffer handles the redirect. bulk = AnalogDataPayload.from_buffer(raw) - df = bulk.to_dataframe() + df = to_dataframe(bulk) assert list(df.columns) == ["analog_input0", "encoder", "analog_input1"] assert len(df) == 2 assert df["analog_input0"].tolist() == [1, 4] @@ -379,13 +380,13 @@ class DeviceName(RegisterBase): assert parsed == "Behavior" # to_dataframe decodes both a single record and a batch. - assert PayloadDeviceName("Behavior").to_dataframe()["value"].tolist() == ["Behavior"] + assert to_dataframe(PayloadDeviceName("Behavior"))["value"].tolist() == ["Behavior"] two = ( PayloadDeviceName("Foo").raw_payload.tobytes() + PayloadDeviceName("Bar").raw_payload.tobytes() ) batch = PayloadDeviceName.from_buffer(two) - assert batch.to_dataframe()["value"].tolist() == ["Foo", "Bar"] + assert to_dataframe(batch)["value"].tolist() == ["Foo", "Bar"] def test_anonymous_payload_scalar_converter_roundtrip(): diff --git a/tests/protocol/test_register_modeling.py b/tests/protocol/test_register_modeling.py index 2656d2e..006e20c 100644 --- a/tests/protocol/test_register_modeling.py +++ b/tests/protocol/test_register_modeling.py @@ -5,6 +5,7 @@ import numpy as np import pytest +from harp.data import to_dataframe from harp.protocol import HarpMessage, HarpVersion from tests.protocol.register_models import ( AnalogData, @@ -208,7 +209,7 @@ def test_complex_configuration_to_dataframe(): Delta=np.uint32(42), ) batch = ComplexConfigurationPayload.from_buffer(cc.raw_payload.tobytes() * 2) - df = batch.to_dataframe() + df = to_dataframe(batch) assert len(df) == 2 assert list(df["PwmPort"]) == ["Pwm2", "Pwm2"] np.testing.assert_array_equal(df["Delta"], [42, 42]) diff --git a/tests/test_device.py b/tests/test_device.py index 4dd49e9..29cf401 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -4,6 +4,7 @@ import numpy as np +from harp.data import to_dataframe from harp.protocol._builder import build_message_frame from harp.protocol._message_type import MessageType from harp.protocol._payload import PayloadBase, BitFlag, GroupMask @@ -124,7 +125,7 @@ def test_op_ctrl_to_dataframe(): _make_op_ctrl_byte(OperationMode.STANDBY, EnableFlag.DISABLED), ] p = OperationControlPayload.from_buffer(bytes(vals)) - df = p.to_dataframe(decode_enums=False) + df = to_dataframe(p, decode_enums=False) assert list(df.columns) == list(OperationControlPayload._repr_fields) assert len(df) == 2 np.testing.assert_array_equal(df["heartbeat"], [True, False]) @@ -153,7 +154,7 @@ def test_pins_batch_ndarray(): def test_pins_to_dataframe(): p = PinsPayload.from_buffer(bytes([0b00000101, 0b00000010])) - df = p.to_dataframe() + df = to_dataframe(p) assert list(df.columns) == list(PinsPayload._repr_fields) assert len(df) == 2 @@ -220,7 +221,7 @@ def test_read_frames_to_dataframe(): vals = [_make_op_ctrl_byte(OperationMode.ACTIVE), _make_op_ctrl_byte(), _make_op_ctrl_byte()] raw = _make_frames(vals) _, payload = _read_frames(raw) - df = payload.to_dataframe() + df = to_dataframe(payload) assert len(df) == 3 assert "heartbeat" in df.columns assert "operation_mode" in df.columns diff --git a/uv.lock b/uv.lock index 041f33d..2cb1996 100644 --- a/uv.lock +++ b/uv.lock @@ -13,6 +13,7 @@ resolution-markers = [ [manifest] members = [ "harp", + "harp-data", "harp-device", "harp-protocol", "harp-serial", @@ -348,6 +349,7 @@ name = "harp" version = "0.2.0" source = { virtual = "." } dependencies = [ + { name = "harp-data" }, { name = "harp-device" }, { name = "harp-protocol" }, { name = "harp-serial" }, @@ -355,6 +357,7 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "harp-data" }, { name = "harp-device" }, { name = "harp-protocol" }, { name = "harp-serial" }, @@ -378,6 +381,7 @@ docs = [ [package.metadata] requires-dist = [ + { name = "harp-data", editable = "src/packages/harp-data" }, { name = "harp-device", editable = "src/packages/harp-device" }, { name = "harp-protocol", editable = "src/packages/harp-protocol" }, { name = "harp-serial", editable = "src/packages/harp-serial" }, @@ -385,6 +389,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ + { name = "harp-data", editable = "src/packages/harp-data" }, { name = "harp-device", editable = "src/packages/harp-device" }, { name = "harp-protocol", editable = "src/packages/harp-protocol" }, { name = "harp-serial", editable = "src/packages/harp-serial" }, @@ -406,6 +411,23 @@ docs = [ { name = "mkdocstrings-python", specifier = ">=1.16.6" }, ] +[[package]] +name = "harp-data" +version = "0.1.0" +source = { editable = "src/packages/harp-data" } +dependencies = [ + { name = "harp-protocol" }, + { name = "numpy" }, + { name = "pandas" }, +] + +[package.metadata] +requires-dist = [ + { name = "harp-protocol", editable = "src/packages/harp-protocol" }, + { name = "numpy", specifier = ">=1.24" }, + { name = "pandas", specifier = ">=2.0" }, +] + [[package]] name = "harp-device" version = "0.1.0" @@ -423,14 +445,12 @@ version = "0.4.0" source = { editable = "src/packages/harp-protocol" } dependencies = [ { name = "numpy" }, - { name = "pandas" }, { name = "typing-extensions" }, ] [package.metadata] requires-dist = [ { name = "numpy", specifier = ">=1.24" }, - { name = "pandas", specifier = ">=2.0" }, { name = "typing-extensions", specifier = ">=4.0" }, ] From 58cbc55d31b0d4f8f68108aec1ece5e872c7f690 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 26 Jun 2026 04:55:38 -0700 Subject: [PATCH 241/267] Add minor changes to documentation --- .../src/harp/protocol/_message.py | 13 ++++++-- .../src/harp/protocol/_message_type.py | 2 ++ .../src/harp/protocol/_payload.py | 13 ++++++-- .../src/harp/protocol/_payload_converters.py | 30 ++++++++++++------- .../src/harp/protocol/_payload_type.py | 1 + tests/protocol/register_models.py | 2 -- 6 files changed, 44 insertions(+), 17 deletions(-) diff --git a/src/packages/harp-protocol/src/harp/protocol/_message.py b/src/packages/harp-protocol/src/harp/protocol/_message.py index ef522a6..9192245 100644 --- a/src/packages/harp-protocol/src/harp/protocol/_message.py +++ b/src/packages/harp-protocol/src/harp/protocol/_message.py @@ -20,6 +20,8 @@ class HarpParseError(Exception): + """An exception raised for errors encountered during message parsing""" + pass @@ -136,7 +138,7 @@ def __str__(self) -> str: class ParsedHarpMessage(HarpMessage, Generic[P]): """A ``HarpMessage`` with a typed parsed payload attached.""" - __slots__ = ("parsed",) + __slots__ = ("_parsed",) def __init__( self, @@ -152,12 +154,17 @@ def __init__( super().__init__( message_type, address, payload_type, payload, port=port, timestamp=timestamp ) - self.parsed = parsed + self._parsed = parsed @classmethod def from_message(cls, msg: HarpMessage, parsed: P) -> "ParsedHarpMessage[P]": """Wrap a ``HarpMessage`` with a pre-parsed payload.""" obj = cls.__new__(cls) obj._bytes = msg.bytes - obj.parsed = parsed + obj._parsed = parsed return obj + + @property + def parsed(self) -> P: + """Returns the parsed payload.""" + return self._parsed diff --git a/src/packages/harp-protocol/src/harp/protocol/_message_type.py b/src/packages/harp-protocol/src/harp/protocol/_message_type.py index 623562e..6ac97dd 100644 --- a/src/packages/harp-protocol/src/harp/protocol/_message_type.py +++ b/src/packages/harp-protocol/src/harp/protocol/_message_type.py @@ -2,6 +2,8 @@ class MessageType(IntEnum): + """Represents the a message type from the harp protocol""" + Read = 1 Write = 2 Event = 3 diff --git a/src/packages/harp-protocol/src/harp/protocol/_payload.py b/src/packages/harp-protocol/src/harp/protocol/_payload.py index 247508b..9cdb7c7 100644 --- a/src/packages/harp-protocol/src/harp/protocol/_payload.py +++ b/src/packages/harp-protocol/src/harp/protocol/_payload.py @@ -110,6 +110,7 @@ def __init__( offset: int = 0, default: object = _MISSING, ) -> None: + """Instantiates a new payload Field.""" self._converter = converter self._name: str | None = None self._mask = mask @@ -143,6 +144,7 @@ def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: return self._converter.decode_scalar(obj._arr[self._name]) # ty: ignore[invalid-argument-type] def _to_batch(self) -> "_FieldBatch[T]": + """Returns the metadata for the corresponding Batch type""" return _FieldBatch( converter=self._converter, mask=self._mask, @@ -165,6 +167,7 @@ class BitFlag: def __new__(cls, *, mask: int, offset: int = 0, default: bool = ...) -> bool: ... # type: ignore[misc] # noqa: E704 def __init__(self, *, mask: int, offset: int = 0, default: object = _MISSING) -> None: + """Instantiates a new BitFlag field attribute for the payload""" self._mask = mask self._offset = offset self._default = default @@ -182,6 +185,7 @@ def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: return bool(obj._arr[self._slot] & self._mask) def _to_batch(self) -> "_BitFlagBatch": + """Returns the metadata for the corresponding batch type""" return _BitFlagBatch(self._mask, slot=self._slot, dtype=self._dtype) @@ -200,7 +204,7 @@ def _build_enum_lookup(enum_cls: type[enum.IntEnum]) -> "tuple[list[str], np.nda class GroupMask(Generic[E]): """Descriptor for a masked, shifted enum sub-field of a payload element. - Readable sugar over a masked :class:`Field`: the raw value is extracted as + Syntactic sugar over a masked :class:`Field`: the raw value is extracted as ``(element & mask) >> shift`` and mapped strictly to an ``enum.IntEnum`` member (an unknown code raises). ``enum=`` is required; for masked *numeric* fields use ``Field(converter=..., mask=...)`` instead. @@ -225,6 +229,7 @@ def __init__( offset: int = 0, default: object = _MISSING, ) -> None: + """Instantiates a GroupMask field for the payload""" if enum is None: raise TypeError( "GroupMask requires 'enum'; use Field(converter=..., mask=...) for " @@ -259,6 +264,7 @@ def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: return self._decode_raw(raw) def _to_batch(self) -> "_GroupMaskBatch[E]": + """Returns the metadata for the corresponding batch type""" return _GroupMaskBatch( self._mask, self._enum, @@ -360,7 +366,7 @@ def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: return (obj._arr[self._slot] & self._mask) >> self._shift -_PT = TypeVar("_PT", bound="PayloadBase[Any]") +_PT = TypeVar("_PT", bound="PayloadBase[Any]", covariant=True) _MISSING_INIT = Sentinel("_MISSING_INIT") @@ -632,6 +638,7 @@ def _mro_descriptor(cls, name: str) -> object | None: def _collect_bitfields( cls, ) -> "dict[str, BitFlag | GroupMask | _BitFlagBatch | _GroupMaskBatch]": + """Collects all bit-field-like members of the payload""" out: dict[str, Any] = {} for klass in reversed(cls.__mro__): for attr, val in klass.__dict__.items(): @@ -641,6 +648,7 @@ def _collect_bitfields( @classmethod def _collect_defaults(cls) -> "dict[str, Any]": + """Collect all members with defined default values""" out: dict[str, Any] = {} for klass in reversed(cls.__mro__): for attr, val in klass.__dict__.items(): @@ -739,6 +747,7 @@ def raw_payload(self) -> NDArray[NpStructT]: return self._arr def to_columns(self, *, decode_enums: bool = True) -> list[Column]: + """Returns a list of Column where each member represents a field from a payload across multiple messages""" arr = np.atleast_1d(self._arr) cls = type(self) repr_fields = self._repr_fields diff --git a/src/packages/harp-protocol/src/harp/protocol/_payload_converters.py b/src/packages/harp-protocol/src/harp/protocol/_payload_converters.py index 0cc8d0d..b7f4655 100644 --- a/src/packages/harp-protocol/src/harp/protocol/_payload_converters.py +++ b/src/packages/harp-protocol/src/harp/protocol/_payload_converters.py @@ -17,12 +17,10 @@ class Converter(ABC, Generic[T]): """Abstract base for payload field converters. - Subclasses must set ``dtype`` and ``init_kwarg_type`` as class attributes - and implement the three abstract methods. + Subclasses must set ``dtype` as class attribute and implement the abstract methods. """ dtype: np.dtype # dtype of the raw numpy slot passed to decode/encode - init_kwarg_type: type # used for docs/introspection TODO especially to generate the constructor type hints; not enforced at runtime @abstractmethod def decode_scalar(self, view: np.generic) -> T: @@ -47,7 +45,6 @@ class IdentityConverter(Converter[NpScalarT]): def __init__(self, dtype: "np.dtype[NpScalarT] | str | type[NpScalarT]") -> None: self.dtype = np.dtype(dtype) - self.init_kwarg_type = self.dtype.type def decode_scalar(self, view: np.generic) -> NpScalarT: return cast( @@ -62,46 +59,64 @@ def encode_into(self, view: NDArray[np.generic], value: NpScalarT) -> None: class UInt8Converter(IdentityConverter[np.uint8]): + """A built-in UInt8 passthrough converter""" + def __init__(self) -> None: super().__init__(np.uint8) class SInt8Converter(IdentityConverter[np.int8]): + """A built-in Int8 passthrough converter""" + def __init__(self) -> None: super().__init__(np.int8) class UInt16Converter(IdentityConverter[np.uint16]): + """A built-in UInt16 passthrough converter""" + def __init__(self) -> None: super().__init__(np.uint16) class Int16Converter(IdentityConverter[np.int16]): + """A built-in Int16 passthrough converter""" + def __init__(self) -> None: super().__init__(np.int16) class UInt32Converter(IdentityConverter[np.uint32]): + """A built-in UInt32 passthrough converter""" + def __init__(self) -> None: super().__init__(np.uint32) class Int32Converter(IdentityConverter[np.int32]): + """A built-in Int32 passthrough converter""" + def __init__(self) -> None: super().__init__(np.int32) class UInt64Converter(IdentityConverter[np.uint64]): + """A built-in UInt64 passthrough converter""" + def __init__(self) -> None: super().__init__(np.uint64) class Int64Converter(IdentityConverter[np.int64]): + """A built-in Int64 passthrough converter""" + def __init__(self) -> None: super().__init__(np.int64) class FloatConverter(IdentityConverter[np.float32]): + """A built-in float passthrough converter""" + def __init__(self) -> None: super().__init__(np.float32) @@ -112,8 +127,6 @@ class BoolConverter(Converter[bool]): The element is non-zero → ``True``. Operates on a single base element. """ - init_kwarg_type = bool - def __init__(self, dtype: "np.dtype | str | type" = np.uint8) -> None: self.dtype = np.dtype(dtype) @@ -138,7 +151,6 @@ class EnumConverter(Converter[E]): def __init__(self, enum_cls: "type[E]", dtype: "np.dtype | str | type" = np.uint8) -> None: self._enum = enum_cls self.dtype = np.dtype(dtype) - self.init_kwarg_type = enum_cls def decode_scalar(self, view: np.generic) -> E: return self._enum(int(view)) @@ -186,9 +198,7 @@ def __str__(self) -> str: class HarpVersionConverter(Converter[HarpVersion]): - """Converts a 3-element uint8 array to/from a HarpVersion object.""" - - init_kwarg_type = HarpVersion + """A built-in converter for a HarpVersion object.""" def __init__(self, component: "np.dtype | str | type" = np.uint8) -> None: self.dtype = np.dtype((component, (3,))) diff --git a/src/packages/harp-protocol/src/harp/protocol/_payload_type.py b/src/packages/harp-protocol/src/harp/protocol/_payload_type.py index a955416..c9ee0c0 100644 --- a/src/packages/harp-protocol/src/harp/protocol/_payload_type.py +++ b/src/packages/harp-protocol/src/harp/protocol/_payload_type.py @@ -21,6 +21,7 @@ class PayloadType(Enum): @property def numpy_dtype(self) -> np.dtype: + """Returns the corresponding numpy dtype""" return self.value diff --git a/tests/protocol/register_models.py b/tests/protocol/register_models.py index b5e0748..66b5ab0 100644 --- a/tests/protocol/register_models.py +++ b/tests/protocol/register_models.py @@ -92,8 +92,6 @@ class EncoderModeMask(enum.IntEnum): class BytesToIntConverter(Converter[int]): """N raw bytes (little-endian) <-> Python int. Models ``interfaceType: int`` over a sub-array.""" - init_kwarg_type = int - def __init__(self, length: int, *, signed: bool = False) -> None: self._length = length self._signed = signed From 8011bee68da51321222498858d2812d7627bdf41 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sun, 28 Jun 2026 13:45:02 -0700 Subject: [PATCH 242/267] Remove BitField in favor of BitMask notation --- scripts/core_registers_example.py | 8 +- .../harp-data/src/harp/data/_reader.py | 28 +- .../harp-device/src/harp/device/_registers.py | 48 +- .../src/harp/protocol/__init__.py | 4 +- .../src/harp/protocol/_payload.py | 530 ++++++++++-------- .../src/harp/protocol/_payload_converters.py | 2 +- .../src/harp/protocol/_register.py | 10 +- tests/protocol/register_models.py | 40 +- tests/protocol/test_converter.py | 18 +- tests/protocol/test_payload.py | 4 +- tests/protocol/test_register.py | 24 +- tests/protocol/test_register_modeling.py | 23 +- tests/test_device.py | 70 ++- 13 files changed, 445 insertions(+), 364 deletions(-) diff --git a/scripts/core_registers_example.py b/scripts/core_registers_example.py index 24591a8..4669dfa 100644 --- a/scripts/core_registers_example.py +++ b/scripts/core_registers_example.py @@ -10,12 +10,12 @@ from harp.device import ( # Enums / flags + ClockConfigurationFlags, EnableFlag, OperationMode, + ResetFlags, # Payload classes - ClockConfigurationPayload, OperationControlPayload, - ResetDevicePayload, # Registers AssemblyVersion, ClockConfiguration, @@ -51,9 +51,9 @@ heartbeat=EnableFlag.ENABLED, ), ), - (ResetDevice, ResetDevicePayload(restore_default=True, restore_name=True)), + (ResetDevice, ResetFlags.RESTORE_DEFAULT | ResetFlags.RESTORE_NAME), (DeviceName, "my-harp-device"), - (ClockConfiguration, ClockConfigurationPayload(clock_repeater=True, clock_unlock=True)), + (ClockConfiguration, ClockConfigurationFlags.CLOCK_REPEATER | ClockConfigurationFlags.CLOCK_UNLOCK), (HardwareVersionHigh, np.uint8(2)), (HardwareVersionLow, np.uint8(0)), (AssemblyVersion, np.uint8(3)), diff --git a/src/packages/harp-data/src/harp/data/_reader.py b/src/packages/harp-data/src/harp/data/_reader.py index b015c18..98a27f7 100644 --- a/src/packages/harp-data/src/harp/data/_reader.py +++ b/src/packages/harp-data/src/harp/data/_reader.py @@ -19,6 +19,8 @@ def _read_bytes(source: Source) -> bytes: return source.read() return Path(source).read_bytes() +_DEFAULT_COLUMN_NAME = "value" + def columns_to_dataframe(columns: list[Column]) -> pd.DataFrame: """Assemble :class:`~harp.protocol.Column` objects into a DataFrame. @@ -28,7 +30,7 @@ def columns_to_dataframe(columns: list[Column]) -> pd.DataFrame: """ return pd.DataFrame( { - c.name: ( + (c.name if c.name is not None else _DEFAULT_COLUMN_NAME): ( pd.Categorical.from_codes(c.data, categories=c.categories) if c.categories is not None else c.data @@ -38,9 +40,20 @@ def columns_to_dataframe(columns: list[Column]) -> pd.DataFrame: ) -def to_dataframe(payload: Any, *, decode_enums: bool = True) -> pd.DataFrame: - """Turn a (batched) payload into a DataFrame, one row per frame.""" - return columns_to_dataframe(payload.to_columns(decode_enums=decode_enums)) +def to_dataframe( + payload: Any, *, decode_enums: bool = True, demux_bit_masks: bool = False +) -> pd.DataFrame: + """Turn a (batched) payload into a DataFrame, one row per frame. + + ``decode_enums`` relabels enum columns as ``pd.Categorical``; ``demux_bit_masks`` + expands each flag (``BitMask``) column into one boolean column per flag member. + """ + # TODO: we may need to account for cases where columns have the same name. + # this can happen when demuxing bitmasks, for example, where each bitmask column + # is expanded into multiple boolean columns with the same name. + return columns_to_dataframe( + payload.to_columns(decode_enums=decode_enums, demux_bit_masks=demux_bit_masks) + ) def read_dataframe( @@ -50,16 +63,19 @@ def read_dataframe( timestamp: bool = True, message_type: bool = False, decode_enums: bool = True, + demux_bit_masks: bool = False, ) -> pd.DataFrame: """Parse all frames of ``register`` from ``source`` into a DataFrame. ``source`` may be a file path, raw bytes, or an open binary file object. ``timestamp`` and ``message_type`` insert leading columns; ``decode_enums`` - controls whether enum fields become ``pd.Categorical`` (True) or raw codes. + controls whether enum fields become ``pd.Categorical`` (True) or raw codes; + ``demux_bit_masks`` expands each flag (``BitMask``) field into one boolean + column per flag member (True) or keeps it as a single raw-integer column. """ raw = _read_bytes(source) _data, timestamps, msg_view, payload = register.parse_bulk(raw, parse_timestamp=timestamp) - df = to_dataframe(payload, decode_enums=decode_enums) + df = to_dataframe(payload, decode_enums=decode_enums, demux_bit_masks=demux_bit_masks) if message_type and msg_view is not None: df.insert( diff --git a/src/packages/harp-device/src/harp/device/_registers.py b/src/packages/harp-device/src/harp/device/_registers.py index 85eeb5e..b489da6 100644 --- a/src/packages/harp-device/src/harp/device/_registers.py +++ b/src/packages/harp-device/src/harp/device/_registers.py @@ -7,7 +7,9 @@ import numpy as np from harp.protocol import ( AnonymousPayload, - BitFlag, + BitMask, + BoolConverter, + Field, GroupMask, PayloadType, RegisterBase, @@ -80,9 +82,9 @@ class OperationControlPayload(StructPayload[np.uint8]): operation_mode: OperationMode = GroupMask(enum=OperationMode, mask=0x3) """Specifies the operation mode of the device.""" - dump_registers: bool = BitFlag(mask=0x8) + dump_registers: bool = Field(BoolConverter(), mask=0x8) """Specifies whether the device should report the content of all registers on initialization.""" - mute_replies: bool = BitFlag(mask=0x10) + mute_replies: bool = Field(BoolConverter(), mask=0x10) """Specifies whether the replies to all commands will be muted, i.e. not sent by the device.""" visual_indicators: EnableFlag = GroupMask(enum=EnableFlag, mask=0x20) """Specifies the state of all visual indicators on the device.""" @@ -92,44 +94,22 @@ class OperationControlPayload(StructPayload[np.uint8]): """Specifies whether the device should report the content of the seconds register each second.""" -class ResetDevicePayload(StructPayload[np.uint8]): +class ResetDevicePayload(AnonymousPayload[np.uint8]): """Represents the payload of the ResetDevice register.""" - restore_default: bool = BitFlag(mask=0x1) - """The device will boot with all the registers reset to their default factory values.""" - restore_eeprom: bool = BitFlag(mask=0x2) - """The device will boot and restore all the registers to the values stored in non-volatile memory.""" - save: bool = BitFlag(mask=0x4) - """The device will boot and save all the current register values to non-volatile memory.""" - restore_name: bool = BitFlag(mask=0x8) - """The device will boot with the default device name.""" - update_firmware: bool = BitFlag(mask=0x20) - """The device will enter firmware update mode.""" - boot_from_default: bool = BitFlag(mask=0x40) - """Specifies that the device has booted from default factory values.""" - boot_from_eeprom: bool = BitFlag(mask=0x80) - """Specifies that the device has booted from non-volatile values stored in EEPROM.""" + __value__: ResetFlags = BitMask(enum=ResetFlags) -class DeviceNamePayload(AnonymousPayload, converter=StringConverter(25)): +class DeviceNamePayload(AnonymousPayload[np.uint8]): """Represents the payload of the DeviceName register.""" + __value__: str = Field(StringConverter(25)) -class ClockConfigurationPayload(StructPayload[np.uint8]): + +class ClockConfigurationPayload(AnonymousPayload[np.uint8]): """Represents the payload of the ClockConfiguration register.""" - clock_repeater: bool = BitFlag(mask=0x1) - """The device will repeat the clock synchronization signal to the clock output connector, if available.""" - clock_generator: bool = BitFlag(mask=0x2) - """The device resets and generates the clock synchronization signal on the clock output connector, if available.""" - repeater_capability: bool = BitFlag(mask=0x8) - """Specifies the device has the capability to repeat the clock synchronization signal to the clock output connector.""" - generator_capability: bool = BitFlag(mask=0x10) - """Specifies the device has the capability to generate the clock synchronization signal to the clock output connector.""" - clock_unlock: bool = BitFlag(mask=0x40) - """The device will unlock the timestamp register counter and will accept commands to set new timestamp values.""" - clock_lock: bool = BitFlag(mask=0x80) - """The device will lock the timestamp register counter and will not accept commands to set new timestamp values.""" + __value__: ClockConfigurationFlags = BitMask(enum=ClockConfigurationFlags) class WhoAmI(RegisterU16): @@ -200,7 +180,7 @@ class OperationControl(RegisterBase[OperationControlPayload]): payload_class = OperationControlPayload -class ResetDevice(RegisterBase[ResetDevicePayload]): +class ResetDevice(RegisterBase[ResetFlags]): """Resets the device and saves non-volatile registers.""" address: ClassVar[int] = 11 @@ -222,7 +202,7 @@ class SerialNumber(RegisterU16): address: ClassVar[int] = 13 -class ClockConfiguration(RegisterBase[ClockConfigurationPayload]): +class ClockConfiguration(RegisterBase[ClockConfigurationFlags]): """Specifies the configuration for the device synchronization clock.""" address: ClassVar[int] = 14 diff --git a/src/packages/harp-protocol/src/harp/protocol/__init__.py b/src/packages/harp-protocol/src/harp/protocol/__init__.py index 8f42beb..6e2d57e 100644 --- a/src/packages/harp-protocol/src/harp/protocol/__init__.py +++ b/src/packages/harp-protocol/src/harp/protocol/__init__.py @@ -13,8 +13,8 @@ PayloadBase, StructPayload, Field, - BitFlag, GroupMask, + BitMask, PayloadU8, PayloadU16, PayloadU32, @@ -82,8 +82,8 @@ "AnonymousPayload", "Column", "Field", - "BitFlag", "GroupMask", + "BitMask", "PayloadU8", "PayloadU16", "PayloadU32", diff --git a/src/packages/harp-protocol/src/harp/protocol/_payload.py b/src/packages/harp-protocol/src/harp/protocol/_payload.py index 9cdb7c7..55fcc6b 100644 --- a/src/packages/harp-protocol/src/harp/protocol/_payload.py +++ b/src/packages/harp-protocol/src/harp/protocol/_payload.py @@ -22,6 +22,7 @@ NpStructT = TypeVar("NpStructT", bound=np.generic) T = TypeVar("T") E = TypeVar("E", bound=enum.IntEnum) +F = TypeVar("F", bound=enum.IntFlag) _MISSING = Sentinel("_MISSING") _DEFAULT_ELEMENT = np.dtype(np.uint8) @@ -38,9 +39,11 @@ class Column: ``eq=False`` keeps identity comparison — field-wise equality would hit numpy's ambiguous-truth-value error on the ``data`` array. + + ``name`` is ``None`` for an anonymous single value """ - name: str + name: str | None data: NDArray[Any] categories: Any | None = None @@ -82,7 +85,7 @@ class Field(Generic[T]): and then run through ``converter`` (which dictates the output type). The right-shift is derived from ``mask`` (its trailing-zero count). Several masked fields at the same offset share the element slot automatically, and may share - it with a :class:`GroupMask` or :class:`BitFlag` on the same word. + it with a :class:`GroupMask` or :class:`BitMask` on the same word. ``offset`` defaults to ``0``. Omitting it suits a payload with a single member; when a payload has several distinct slots, each must declare an @@ -112,18 +115,14 @@ def __init__( ) -> None: """Instantiates a new payload Field.""" self._converter = converter - self._name: str | None = None self._mask = mask self._shift = _mask_trailing_zeros(mask) if mask is not None else 0 self._offset = offset self._default = default - # Derived in PayloadBase.__init_subclass__ for the masked variant: - self._slot: str = "value" + # Numpy slot this field reads/writes; assigned in PayloadBase.__init_subclass__. + self._slot: str = "" self._dtype: np.dtype = _DEFAULT_ELEMENT - def __set_name__(self, owner: object, name: str) -> None: - self._name = name - def _encode_value(self, value: Any) -> int: """Map a user value back to the integer to be masked + shifted into the slot (masked variant only).""" @@ -131,6 +130,12 @@ def _encode_value(self, value: Any) -> int: self._converter.encode_into(tmp, value) return int(tmp) + def _bind_slot(self, slot: str, elem: np.dtype) -> None: + """Bind this field to its numpy ``slot`` (called by ``_build_struct_dtype``).""" + self._slot = slot + if self._mask is not None: # masked sub-field reads the base element + self._dtype = elem + @overload def __get__(self, obj: None, owner: object = None) -> "Field[T]": ... @overload @@ -141,7 +146,7 @@ def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: if self._mask is not None: raw = (obj._arr[self._slot] & self._mask) >> self._shift return self._converter.decode_scalar(self._converter.dtype.type(raw)) - return self._converter.decode_scalar(obj._arr[self._name]) # ty: ignore[invalid-argument-type] + return self._converter.decode_scalar(obj._arr[self._slot]) # ty: ignore[invalid-argument-type] def _to_batch(self) -> "_FieldBatch[T]": """Returns the metadata for the corresponding Batch type""" @@ -152,41 +157,26 @@ def _to_batch(self) -> "_FieldBatch[T]": dtype=self._dtype, ) - -class BitFlag: - """Descriptor for a single bit within a payload element, exposed as ``bool``. - - Reads the base element at ``offset`` (base-element units; defaults to ``0``) - and tests ``element & mask``. The element width and the physical storage slot - are derived from the payload's base element type, so several bit flags at the - same offset automatically share storage. - """ - - if TYPE_CHECKING: - - def __new__(cls, *, mask: int, offset: int = 0, default: bool = ...) -> bool: ... # type: ignore[misc] # noqa: E704 - - def __init__(self, *, mask: int, offset: int = 0, default: object = _MISSING) -> None: - """Instantiates a new BitFlag field attribute for the payload""" - self._mask = mask - self._offset = offset - self._default = default - # Derived in PayloadBase.__init_subclass__: - self._slot: str = "value" - self._dtype: np.dtype = _DEFAULT_ELEMENT - - @overload - def __get__(self, obj: None, owner: object = None) -> "BitFlag": ... - @overload - def __get__(self, obj: "PayloadBase", owner: object = None) -> bool: ... - def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: - if obj is None: - return self - return bool(obj._arr[self._slot] & self._mask) - - def _to_batch(self) -> "_BitFlagBatch": - """Returns the metadata for the corresponding batch type""" - return _BitFlagBatch(self._mask, slot=self._slot, dtype=self._dtype) + def _columns( + self, arr: "NDArray[Any]", name: "str | None", *, decode_enums: bool, demux_bit_masks: bool + ) -> "list[Column]": + """Render this field as one or more batched :class:`Column`s (see ``to_columns``). + + ``name`` is the column name, or ``None`` for an anonymous root value (the + consuming package decides the fallback label).""" + if self._mask is not None: # masked numeric sub-field + raw = (arr[self._slot] & self._mask) >> self._shift + return [Column(name, self._converter.decode_batch(raw.astype(self._converter.dtype)))] + if not isinstance(self._converter, _IdentityConverter): # whole-element, decoded + return [Column(name, self._converter.decode_batch(arr[self._slot]))] + sub = arr[self._slot] # whole-element, raw passthrough + if sub.ndim <= 1: + return [Column(name, sub)] + # sub-array -> one column per element; index is intrinsic identity, so a + # nameless (root) array is positional, a named field is prefixed. + flat = sub.reshape(len(arr), -1) + label = (lambda i: str(i)) if name is None else (lambda i: f"{name}_{i}") + return [Column(label(i), flat[:, i]) for i in range(flat.shape[1])] def _build_enum_lookup(enum_cls: type[enum.IntEnum]) -> "tuple[list[str], np.ndarray]": @@ -240,8 +230,8 @@ def __init__( self._enum = enum self._offset = offset self._default = default - # Derived in PayloadBase.__init_subclass__: - self._slot: str = "value" + # Numpy slot assigned in PayloadBase.__init_subclass__. + self._slot: str = "" self._dtype: np.dtype = _DEFAULT_ELEMENT self._categories, self._code_lookup = _build_enum_lookup(enum) @@ -253,6 +243,11 @@ def _encode_value(self, value: Any) -> int: """Map a user value back to the integer to be masked + shifted into the slot.""" return int(value) + def _bind_slot(self, slot: str, elem: np.dtype) -> None: + """Bind this group mask to its shared numpy ``slot``.""" + self._slot = slot + self._dtype = elem + @overload def __get__(self, obj: None, owner: object = None) -> "GroupMask[E]": ... @overload @@ -272,6 +267,108 @@ def _to_batch(self) -> "_GroupMaskBatch[E]": dtype=self._dtype, ) + def _columns( + self, arr: "NDArray[Any]", name: "str | None", *, decode_enums: bool, demux_bit_masks: bool + ) -> "list[Column]": + """One enum column: category codes + labels (``decode_enums``) or raw codes.""" + raw = (arr[self._slot] & self._mask) >> self._shift + if decode_enums: + return [Column(name, self._code_lookup[raw], self._categories)] + return [Column(name, raw)] + + +class BitMask(Generic[F]): + """Descriptor for a masked ``enum.IntFlag`` view of a payload element. + + The flag counterpart of :class:`GroupMask`: the raw value is extracted as + ``element & mask`` and mapped to an ``enum.IntFlag`` member (decoding is + *permissive* — combined flag values such as ``A | B`` are valid, matching the + C# generator's unchecked cast). ``enum=`` is required and must be an + ``IntFlag`` subclass. + + Unlike :class:`GroupMask` there is **no shift**: ``IntFlag`` member values are + absolute bit positions, so the flags are read and written in place. ``mask`` + defaults to the full base element (the common whole-register bitMask case) and + may be narrowed to embed a flag set inside a wider element. The element width + and storage slot are derived from the payload's base element type, so several + masked fields at the same offset share storage automatically. + """ + + if TYPE_CHECKING: + # flag variant -> the field type is the IntFlag + def __new__( # type: ignore[misc] # noqa: E704 + cls, *, enum: "type[F]", mask: int | None = None, offset: int = 0, default: "F" = ... + ) -> "F": ... + + def __init__( + self, + *, + enum: type[F], + mask: int | None = None, + offset: int = 0, + default: object = _MISSING, + ) -> None: + """Instantiates a BitMask field for the payload""" + if enum is None: + raise TypeError("BitMask requires 'enum' (an enum.IntFlag subclass)") + # mask=None is resolved to the full base element in _build_struct_dtype. + self._mask = mask + self._shift = 0 # IntFlag members are absolute bit positions; never shifted + self._enum = enum + self._offset = offset + self._default = default + # Numpy slot assigned in PayloadBase.__init_subclass__. + self._slot: str = "" + self._dtype: np.dtype = _DEFAULT_ELEMENT + + def _decode_raw(self, raw: Any) -> Any: + """Map an extracted (masked) integer to its IntFlag value (permissive).""" + return self._enum(int(raw)) + + def _encode_value(self, value: Any) -> int: + """Map a user value back to the integer to be masked into the slot.""" + return int(value) + + def _bind_slot(self, slot: str, elem: np.dtype) -> None: + """Bind this flag mask to its shared numpy ``slot``; default the mask to the full element.""" + self._slot = slot + self._dtype = elem + if self._mask is None: + self._mask = (1 << (elem.itemsize * 8)) - 1 + + @overload + def __get__(self, obj: None, owner: object = None) -> "BitMask[F]": ... + @overload + def __get__(self, obj: "PayloadBase", owner: object = None) -> F: ... + def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: + if obj is None: + return self + raw = obj._arr[self._slot] & self._mask + return self._decode_raw(raw) + + def _to_batch(self) -> "_BitMaskBatch[F]": + """Returns the metadata for the corresponding batch type""" + return _BitMaskBatch( + self._mask, + self._enum, + slot=self._slot, + dtype=self._dtype, + ) + + def _columns( + self, arr: "NDArray[Any]", name: "str | None", *, decode_enums: bool, demux_bit_masks: bool + ) -> "list[Column]": + """A single raw-integer column, or (``demux_bit_masks``) one bool column per flag member.""" + raw = arr[self._slot] & self._mask + if not demux_bit_masks: + return [Column(name, raw)] + # One boolean column per flag member that fits the field. + return [ + Column(member.name, (raw & int(member)) != 0) + for member in self._enum + if not (int(member) & ~self._mask) # skip bits that can't fit the field + ] + # --------------------------------------------------------------------------- # Descriptors — batch variants (return ndarray views) @@ -287,19 +384,15 @@ def __init__( *, converter: _Converter[T], mask: int | None = None, - slot: str = "value", + slot: str = "", dtype: "np.dtype | str | type" = np.uint8, ) -> None: self._converter = converter - self._name: str | None = None self._mask = mask self._shift = _mask_trailing_zeros(mask) if mask is not None else 0 self._slot = slot self._dtype = np.dtype(dtype) - def __set_name__(self, owner: object, name: str) -> None: - self._name = name - @overload def __get__(self, obj: None, owner: object = None) -> "_FieldBatch[T]": ... @overload @@ -310,31 +403,34 @@ def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: if self._mask is not None: raw = (obj._arr[self._slot] & self._mask) >> self._shift return self._converter.decode_batch(raw.astype(self._converter.dtype)) - return self._converter.decode_batch(obj._arr[self._name]) + return self._converter.decode_batch(obj._arr[self._slot]) -class _BitFlagBatch: - """Same as _BitFlag but returns an NDArray view for batch payloads rather than a scalar value.""" +class _BitMaskBatch(Generic[F]): + """Same as BitMask but returns an NDArray view for batch payloads rather than a scalar value.""" def __init__( self, mask: int, + enum: type[F], *, - slot: str = "value", + slot: str = "", dtype: "np.dtype | str | type" = np.uint8, ) -> None: self._mask = mask + self._shift = 0 + self._enum = enum self._slot = slot self._dtype = np.dtype(dtype) @overload - def __get__(self, obj: None, owner: object = None) -> "_BitFlagBatch": ... + def __get__(self, obj: None, owner: object = None) -> "_BitMaskBatch[F]": ... @overload - def __get__(self, obj: "PayloadBase", owner: object = None) -> "NDArray[np.bool_]": ... + def __get__(self, obj: "PayloadBase", owner: object = None) -> "NDArray[Any]": ... def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: if obj is None: return self - return (obj._arr[self._slot] & self._mask) != 0 + return obj._arr[self._slot] & self._mask class _GroupMaskBatch(Generic[E]): @@ -345,7 +441,7 @@ def __init__( mask: int, enum: type[E], *, - slot: str = "value", + slot: str = "", dtype: "np.dtype | str | type" = np.uint8, ) -> None: self._mask = mask @@ -389,30 +485,17 @@ class Batch(Protocol[_PT]): def __len__(self) -> int: ... # type: ignore[empty-body] - def to_columns(self, *, decode_enums: bool = True) -> "list[Column]": ... # type: ignore[empty-body] + def to_columns( # type: ignore[empty-body] + self, *, decode_enums: bool = True, demux_bit_masks: bool = False + ) -> "list[Column]": ... def __getattr__(self, name: str) -> "NDArray[Any]": ... # type: ignore[empty-body] # Helpers for type checking using isinstance() -_SCALAR_DECLARATION_TYPES = (Field, BitFlag, GroupMask) -_BATCH_DECLARATION_TYPES = (_FieldBatch, _BitFlagBatch, _GroupMaskBatch) +_SCALAR_DECLARATION_TYPES = (Field, GroupMask, BitMask) +_BATCH_DECLARATION_TYPES = (_FieldBatch, _GroupMaskBatch, _BitMaskBatch) _DECLARATION_TYPES = _SCALAR_DECLARATION_TYPES + _BATCH_DECLARATION_TYPES -_BITFIELD_TYPES = (BitFlag, GroupMask, _BitFlagBatch, _GroupMaskBatch) -_FIELD_TYPES = (Field, _FieldBatch) -_GROUP_MASK_TYPES = (GroupMask, _GroupMaskBatch) -_BIT_FLAG_TYPES = (BitFlag, _BitFlagBatch) - - -def _is_masked(desc: object) -> bool: - """A descriptor that reads a masked + shifted sub-field of a shared element slot: - every ``BitFlag``/``GroupMask`` (always masked), or a ``Field`` declaring ``mask=``. - Unmasked ``Field`` (whole-element view, own slot) returns False.""" - if isinstance(desc, _BITFIELD_TYPES): - return True - if isinstance(desc, _FIELD_TYPES): - return desc._mask is not None - return False # value/raw_payload deliberately omitted: overriding them is the intended @@ -478,10 +561,14 @@ def _validate_no_overlap(cls: type, slots: "dict[str, _FieldSlot]", itemsize: in def _build_struct_dtype( cls: "type[PayloadBase]", - declarations: "list[tuple[str, Field | BitFlag | GroupMask]]", + declarations: "list[tuple[str, Field | GroupMask | BitMask]]", length: int | None, ) -> np.dtype: - """Build the numpy structured dtype from field declarations.""" + """Build the numpy structured dtype from field declarations. + + Each descriptor binds itself to its numpy slot via ``_bind_slot``; this + function resolves only cross-field layout — which masked fields share a slot, + plus offsets, overlap, and itemsize.""" elem = cls._elem_dtype elem_size = elem.itemsize slots: dict[str, _FieldSlot] = {} @@ -490,25 +577,19 @@ def _build_struct_dtype( for attr_name, val in declarations: byte_offset = val._offset * elem_size # A plain Field (no mask=) is the only whole-element view; everything else - # (BitFlag, GroupMask, or a Field with mask=) is a masked sub-field. The - # isinstance form — rather than _is_masked() — is what lets the type checker - # narrow `val` for the slot/converter attribute access in each branch. + # (GroupMask, BitMask, or a Field with mask=) is a masked sub-field. The + # isinstance form lets the type checker narrow `val` to access ``_converter``. if isinstance(val, Field) and val._mask is None: # whole-element Field — own slot - field_dtype = val._converter.dtype if attr_name in slots: raise TypeError(f"{cls.__name__}: duplicate field name {attr_name!r}") - slots[attr_name] = _FieldSlot(field_dtype, byte_offset) - else: # masked sub-field — share the base-element slot - mask = val._mask - assert mask is not None # invariant for every masked descriptor - val._dtype = elem - _validate_mask_fits(cls, attr_name, mask, elem) - shared_slot = mask_slot_by_byte_offset.get(byte_offset) - if shared_slot is not None: - val._slot = shared_slot - else: - mask_slot_by_byte_offset[byte_offset] = attr_name - val._slot = attr_name + val._bind_slot(attr_name, elem) + slots[attr_name] = _FieldSlot(val._converter.dtype, byte_offset) + else: # masked sub-field — shares the base-element slot at its offset + owner = mask_slot_by_byte_offset.setdefault(byte_offset, attr_name) + val._bind_slot(owner, elem) + assert val._mask is not None # _bind_slot ensures every masked field has a mask + _validate_mask_fits(cls, attr_name, val._mask, elem) + if owner == attr_name: slots[attr_name] = _FieldSlot(elem, byte_offset) if length is not None: @@ -526,23 +607,6 @@ def _build_struct_dtype( ) -def _resolve_single_member( - cls: "type[PayloadBase]", declarations: "list[tuple[str, Any]]" -) -> "str | None": - """A payload with exactly one full-span ``Field`` unwraps to that member on - ``parse`` (register-level ``interfaceType``), avoiding a ``.value`` hop.""" - if len(declarations) != 1: - return None - attr, val = declarations[0] - if ( - isinstance(val, Field) - and val._mask is None - and val._converter.dtype.itemsize == cls.dtype.itemsize # type: ignore[attr-defined] - ): - return attr - return None - - class PayloadBase(Generic[NpStructT]): """Base class for typed Harp register payloads. @@ -562,8 +626,6 @@ class PayloadBase(Generic[NpStructT]): _scalar_cls: ClassVar["type[PayloadBase]"] # The batch twin of this class (identity until the Batch sibling is generated). _batch_cls: ClassVar["type[PayloadBase]"] - # Cached map of attribute name → _BitFlag/_GroupMask descriptor, built once at class definition. - _bitfields: ClassVar[dict[str, Any]] # Cached map of attribute name → default value for fields that declare one. _defaults: ClassVar[dict[str, Any]] # Auto-generated sibling class whose descriptors return NDArray views instead of scalars. @@ -571,7 +633,7 @@ class PayloadBase(Generic[NpStructT]): # Base element dtype (from the ``StructPayload[...]`` type arg); governs offset # arithmetic and the integer width used for masked reads. Defaults to uint8. _elem_dtype: ClassVar[np.dtype] = _DEFAULT_ELEMENT - # Attribute name of the lone full-span member, if any: ``parse`` unwraps to it. + # The ``__value__`` field of an AnonymousPayload root, else None: ``parse`` unwraps to it. _single_member: ClassVar[str | None] = None # The underlying numpy array holding one (0-D) or many (1-D) payload records. _arr: NDArray[NpStructT] @@ -606,14 +668,10 @@ def __init__(self, *args: object, **kwargs: object) -> None: # collides with the first masked field's attribute name. for attr_name, value in kwargs.items(): desc = cls._mro_descriptor(attr_name) - if isinstance(desc, BitFlag): # single bit -> set/clear - mask_in_dtype = np.array(desc._mask, dtype=desc._dtype) - if value: - arr[desc._slot] |= mask_in_dtype - elif isinstance(desc, Field) and desc._mask is None: # whole-element Field - desc._converter.encode_into(arr[desc._name], value) # ty: ignore[invalid-argument-type] - elif isinstance(desc, (GroupMask, Field)): - # masked sub-field (enum or numeric) -> encode, shift, merge into shared slot + if isinstance(desc, Field) and desc._mask is None: # whole-element Field + desc._converter.encode_into(arr[desc._slot], value) # ty: ignore[invalid-argument-type] + elif isinstance(desc, (GroupMask, BitMask, Field)): + # masked sub-field -> encode, shift, merge into the shared slot mask = desc._mask assert mask is not None # invariant for masked descriptors mask_in_dtype = np.array(mask, dtype=desc._dtype) @@ -634,25 +692,13 @@ def _mro_descriptor(cls, name: str) -> object | None: return klass.__dict__[name] return None - @classmethod - def _collect_bitfields( - cls, - ) -> "dict[str, BitFlag | GroupMask | _BitFlagBatch | _GroupMaskBatch]": - """Collects all bit-field-like members of the payload""" - out: dict[str, Any] = {} - for klass in reversed(cls.__mro__): - for attr, val in klass.__dict__.items(): - if isinstance(val, _BITFIELD_TYPES): - out[attr] = val - return out - @classmethod def _collect_defaults(cls) -> "dict[str, Any]": """Collect all members with defined default values""" out: dict[str, Any] = {} for klass in reversed(cls.__mro__): for attr, val in klass.__dict__.items(): - if isinstance(val, (Field, BitFlag, GroupMask)) and val._default is not _MISSING: + if isinstance(val, (Field, GroupMask, BitMask)) and val._default is not _MISSING: out[attr] = val._default return out @@ -707,7 +753,10 @@ def __init_subclass__( if own_declarations: cls.dtype = _build_struct_dtype(cls, own_declarations, length) - cls._single_member = _resolve_single_member(cls, own_declarations) + # Only an AnonymousPayload root (its lone __value__ field) unwraps on + # parse; a StructPayload always returns the wrapper, never auto-unwraps. + if getattr(cls, "_root", False): + cls._single_member = own_declarations[0][0] if "_repr_fields" not in cls.__dict__: cls._repr_fields = cls._collect_repr_fields() @@ -727,7 +776,6 @@ def __init_subclass__( _batch_of=cls, ) - cls._bitfields = cls._collect_bitfields() cls._defaults = cls._collect_defaults() @classmethod @@ -746,60 +794,29 @@ def from_buffer(cls, buf: bytes | bytearray | memoryview) -> Self: def raw_payload(self) -> NDArray[NpStructT]: return self._arr - def to_columns(self, *, decode_enums: bool = True) -> list[Column]: - """Returns a list of Column where each member represents a field from a payload across multiple messages""" + def to_columns( + self, *, decode_enums: bool = True, demux_bit_masks: bool = False + ) -> list[Column]: + """Returns a list of Column where each member represents a field from a payload across multiple messages. + + ``decode_enums`` controls whether ``GroupMask`` (enum) columns become + category codes + labels (True) or raw integer codes (False) — a + shape-preserving relabel. ``demux_bit_masks`` controls whether a + ``BitMask`` (flag) column is expanded into one boolean column per flag + member (True) or kept as a single raw-integer column (False) — a shape + change. The two are orthogonal and apply to different descriptor kinds. + """ arr = np.atleast_1d(self._arr) - cls = type(self) - repr_fields = self._repr_fields - + # Each descriptor renders its own column(s); resolve via the scalar twin. + scalar_cls = type(self)._scalar_cls cols: list[Column] = [] - has_bitfield = any(_is_masked(cls._mro_descriptor(f)) for f in repr_fields) - if has_bitfield: - for f in repr_fields: - desc = cls._mro_descriptor(f) - if isinstance(desc, _GROUP_MASK_TYPES): # masked enum sub-field - slot_col = arr[desc._slot] # ty: ignore[invalid-argument-type] - raw = (slot_col & desc._mask) >> desc._shift - if decode_enums: - cols.append(Column(f, desc._code_lookup[raw], desc._categories)) - else: - cols.append(Column(f, raw)) - elif ( - isinstance(desc, _FIELD_TYPES) and desc._mask is not None - ): # masked numeric sub-field - slot_col = arr[desc._slot] # ty: ignore[invalid-argument-type] - raw = (slot_col & desc._mask) >> desc._shift - decoded = desc._converter.decode_batch(raw.astype(desc._converter.dtype)) - cols.append(Column(f, decoded)) - elif isinstance(desc, _BIT_FLAG_TYPES): - slot_col = arr[desc._slot] # ty: ignore[invalid-argument-type] - cols.append(Column(f, (slot_col & desc._mask) != 0)) - else: - cols.append(Column(f, np.atleast_1d(getattr(self, f)))) - return cols - - names = self.dtype.names - single_value_slot = names == ("value",) - for name in names: # ty: ignore[not-iterable] - desc = cls._mro_descriptor(name) - uses_converter = isinstance(desc, _FIELD_TYPES) and not isinstance( - desc._converter, _IdentityConverter + for f in self._repr_fields: + desc = scalar_cls._mro_descriptor(f) + cols.extend( + desc._columns( # ty: ignore[possibly-unbound-attribute] + arr, f, decode_enums=decode_enums, demux_bit_masks=demux_bit_masks + ) ) - if uses_converter: - cols.append(Column(name, np.atleast_1d(getattr(self, name)))) - continue - - field_dtype, _ = self.dtype.fields[name] # ty: ignore[invalid-assignment, invalid-argument-type, not-subscriptable] - sub = arr[name] # ty: ignore[invalid-argument-type] - if field_dtype.subdtype is None: - cols.append(Column(name, sub)) - else: - _, subshape = field_dtype.subdtype - count = int(np.prod(subshape)) - flat = sub.reshape(len(arr), count) - for i in range(count): - col = str(i) if single_value_slot else f"{name}_{i}" - cols.append(Column(col, flat[:, i])) return cols def __len__(self) -> int: @@ -818,17 +835,11 @@ def __str__(self) -> str: def unwrap(cls, arr: "np.ndarray") -> Any: """Dispatch hook used by ``RegisterBase.parse``. - Struct payloads return a typed wrapper so descriptors like - ``payload.Channel0`` work. Anonymous payloads override this to - return the raw numpy scalar/ndarray directly. - - This allows us to not have to use hacky descriptors for single-field - struct payloads (e.g. a struct with one uint16 field can just be a - PayloadU16 subclass) while still supporting the full descriptor - machinery for multi-field struct payloads. - - A struct payload with exactly one full-span member (register-level - ``interfaceType``) unwraps directly to that member's value. + Struct payloads always return a typed wrapper so descriptors like + ``payload.Channel0`` work. Anonymous payloads override this to return the + raw numpy scalar/ndarray directly, or — for an ``AnonymousPayload`` root — + the unwrapped ``__value__`` (the single-member branch below, reached via + the override's ``super()`` call). A struct payload never auto-unwraps. """ obj = cls.from_array(arr) if cls._single_member is not None and arr.ndim == 0: @@ -843,12 +854,12 @@ def unwrap(cls, arr: "np.ndarray") -> Any: @dataclass_transform( kw_only_default=True, - field_specifiers=(Field, BitFlag, GroupMask), + field_specifiers=(Field, GroupMask, BitMask), ) class StructPayload(PayloadBase[NpStructT]): """Base class for struct register payloads with typed field descriptors. - Subclasses declare fields using ``Field``, ``BitFlag``, or ``GroupMask`` + Subclasses declare fields using ``Field``, ``GroupMask``, or ``BitMask`` descriptors. Type checkers synthesize a keyword-only ``__init__`` from those declarations, so constructor calls are fully type-checked and have IDE autocompletion. @@ -862,7 +873,7 @@ class StructPayload(PayloadBase[NpStructT]): class MyPayload(StructPayload[np.uint8]): channel: np.uint16 = Field(UInt16Converter(), offset=0) - enabled: bool = BitFlag(mask=0x01, offset=2) + flags: MyFlags = BitMask(enum=MyFlags, offset=2) """ @@ -884,39 +895,83 @@ class PayloadU16(AnonymousPayload, scalar_dtype=" None: - if converter is not None: - cls._converter = converter - if scalar_dtype is None: - scalar_dtype = converter.dtype + # A class-body descriptor field selects root mode; it must be named __value__. + body_fields = [ + n for n, v in cls.__dict__.items() if isinstance(v, _SCALAR_DECLARATION_TYPES) + ] + if body_fields: + if scalar_dtype is not None: + raise TypeError( + f"{cls.__name__}: __value__ is mutually exclusive with scalar_dtype=" + ) + if body_fields != [cls._VALUE_FIELD]: + raise TypeError( + f"{cls.__name__}: a single-value (root) payload declares exactly one field " + f"named {cls._VALUE_FIELD!r}; found {body_fields}. Use StructPayload for " + f"multi-field payloads." + ) + cls._root = True + super().__init_subclass__(**kwargs) + return + # Raw scalar slot required, unless a Batch twin / array concrete supplies dtype. + if scalar_dtype is None and "_batch_of" not in kwargs and "dtype" not in cls.__dict__: + raise TypeError( + f"{cls.__name__}: an AnonymousPayload subclass must define its single slot via a " + f"{cls._VALUE_FIELD!r} descriptor field or scalar_dtype= (a codec is a " + f"{cls._VALUE_FIELD!r} Field with a Converter)." + ) if scalar_dtype is not None: cls.dtype = np.dtype(scalar_dtype) cls._repr_fields = () super().__init_subclass__(**kwargs) def __init__(self, value: object = _MISSING_INIT, /, **kwargs: object) -> None: # type: ignore[override] + if type(self)._root: + # root mode: PayloadBase encodes the value into the single __value__ field + if value is not _MISSING_INIT: + super().__init__(value) + else: + super().__init__(**kwargs) + return if value is _MISSING_INIT: if "value" in kwargs: value = kwargs.pop("value") @@ -924,42 +979,39 @@ def __init__(self, value: object = _MISSING_INIT, /, **kwargs: object) -> None: raise TypeError(f"{type(self).__name__}() requires a value") if kwargs: raise TypeError(f"{type(self).__name__}() got unexpected kwargs: {sorted(kwargs)}") - if self._converter is not None: - arr = np.zeros((), dtype=self.dtype) - self._converter.encode_into(arr, value) - self._arr = arr - else: - self._arr = np.asarray(value, dtype=self.dtype) # ty: ignore[invalid-assignment] + self._arr = np.asarray(value, dtype=self.dtype) # ty: ignore[invalid-assignment] @classmethod def unwrap(cls, arr: "np.ndarray") -> Any: - if cls._converter is not None: - return cls._converter.decode_scalar(arr) # ty: ignore[invalid-argument-type] + if cls._root: + return super().unwrap(arr) # PayloadBase single-member unwrap (.__value__) # 0-D → numpy scalar via item-like access (preserves dtype). # 1-D / sub-array → return the ndarray as-is. return arr if arr.ndim > 0 else arr[()] def _repr_kwargs(self) -> str: - if self._converter is not None: - return repr(self._converter.decode_scalar(self._arr)) # ty: ignore[invalid-argument-type] + if type(self)._root: + return super()._repr_kwargs() return repr(self._arr.tolist() if self._arr.ndim > 0 else self._arr[()]) def __repr__(self) -> str: return f"{type(self).__name__}({self._repr_kwargs()})" - def to_columns(self, *, decode_enums: bool = True) -> list[Column]: - if self._converter is not None: - arr = self._arr - # decode_batch wants a leading row axis; a single record lacks one - # (its ndim equals the converter slot's own ndim). - if arr.ndim == self._converter.dtype.ndim: - arr = arr[np.newaxis, ...] - return [Column("value", self._converter.decode_batch(arr))] + def to_columns( + self, *, decode_enums: bool = True, demux_bit_masks: bool = False + ) -> list[Column]: + # Anonymous values carry no name (name=None); the consumer supplies the label. + if type(self)._root: + arr = np.atleast_1d(self._arr) + root = type(self)._scalar_cls._mro_descriptor(self._VALUE_FIELD) + return root._columns( # ty: ignore[possibly-unbound-attribute] + arr, None, decode_enums=decode_enums, demux_bit_masks=demux_bit_masks + ) arr = np.atleast_1d(self._arr) - # Sub-array dtype (array register): shape is already (N, length). + # Sub-array dtype (array register): one column per element, positionally named. if arr.ndim > 1: return [Column(str(i), arr[:, i]) for i in range(arr.shape[1])] - return [Column("value", arr)] + return [Column(None, arr)] @final diff --git a/src/packages/harp-protocol/src/harp/protocol/_payload_converters.py b/src/packages/harp-protocol/src/harp/protocol/_payload_converters.py index b7f4655..560df91 100644 --- a/src/packages/harp-protocol/src/harp/protocol/_payload_converters.py +++ b/src/packages/harp-protocol/src/harp/protocol/_payload_converters.py @@ -122,7 +122,7 @@ def __init__(self) -> None: class BoolConverter(Converter[bool]): - """Whole-element ``interfaceType: bool`` (distinct from a single ``BitFlag`` bit). + """Whole-element ``interfaceType: bool`` (or a single masked bit via ``Field(BoolConverter(), mask=...)``). The element is non-zero → ``True``. Operates on a single base element. """ diff --git a/src/packages/harp-protocol/src/harp/protocol/_register.py b/src/packages/harp-protocol/src/harp/protocol/_register.py index a9fc9e3..6311f51 100644 --- a/src/packages/harp-protocol/src/harp/protocol/_register.py +++ b/src/packages/harp-protocol/src/harp/protocol/_register.py @@ -60,11 +60,17 @@ def __call__(cls: "type[_R]", address: int) -> "type[_R]": class RegisterBase(ABC, Generic[U]): """Abstract base for all typed Harp registers. - The generic parameter ``U`` is the static return type of :meth:`parse`: + The generic parameter ``U`` is the static return type of :meth:`parse` — the + user-facing value, *not* necessarily ``payload_class`` (that is the wire + encoding). The two coincide only for multi-member struct payloads: * scalar registers → a numpy scalar (e.g. ``np.uint16``); * array registers → ``NDArray[…]`` of fixed length; - * structured registers → the payload class itself. + * multi-member struct registers → the payload class itself; + * single-member registers that unwrap on parse → the inner value type, e.g. + ``RegisterBase[str]`` (DeviceName), ``RegisterBase[HarpVersion]``, or + ``RegisterBase[ClockConfigurationFlags]`` for a whole-register ``BitMask`` + / ``GroupMask`` — even though each still has a ``payload_class``. Subclasses must define ``address``, ``payload_type``, and ``payload_class`` as ``ClassVar``s. diff --git a/tests/protocol/register_models.py b/tests/protocol/register_models.py index 66b5ab0..e09e1eb 100644 --- a/tests/protocol/register_models.py +++ b/tests/protocol/register_models.py @@ -32,7 +32,8 @@ from numpy.typing import NDArray from harp.protocol import ( - BitFlag, + AnonymousPayload, + BitMask, BoolConverter, Converter, Field, @@ -190,8 +191,8 @@ class Version(RegisterBase[VersionPayload]): # =========================================================================== -class CustomPayloadPayload(StructPayload[np.uint32], length=3): - value: HarpVersion = Field(HarpVersionConverter(np.uint32)) +class CustomPayloadPayload(AnonymousPayload[np.uint32]): + __value__: HarpVersion = Field(HarpVersionConverter(np.uint32)) class CustomPayload(RegisterBase[HarpVersion]): @@ -200,8 +201,8 @@ class CustomPayload(RegisterBase[HarpVersion]): payload_class = CustomPayloadPayload -class CustomRawPayloadPayload(StructPayload[np.uint32], length=3): - value: HarpVersion = Field(HarpVersionConverter(np.uint32)) +class CustomRawPayloadPayload(AnonymousPayload[np.uint32]): + __value__: HarpVersion = Field(HarpVersionConverter(np.uint32)) class CustomRawPayload(RegisterBase[HarpVersion]): @@ -252,19 +253,17 @@ class Counter0(RegisterS32): # =========================================================================== -# 41 PortDIOSet : U8, Write — bitMask PortDigitalIOS. Bits >= 0x100 do not fit a -# U8 payload; the C# generator truncates them too, so only DIO0..DIO3 model. +# 41 PortDIOSet : U8, Write — bitMask PortDigitalIOS. A single BitMask over the +# whole byte; bits >= 0x100 can't fit a U8 (and the C# generator's byte cast +# drops them too). Single-member -> parse() unwraps to a bare PortDigitalIOS. # =========================================================================== -class PortDIOSetPayload(StructPayload[np.uint8]): - DIO0: bool = BitFlag(mask=0x1) - DIO1: bool = BitFlag(mask=0x2) - DIO2: bool = BitFlag(mask=0x4) - DIO3: bool = BitFlag(mask=0x8) +class PortDIOSetPayload(AnonymousPayload[np.uint8]): + __value__: PortDigitalIOS = BitMask(enum=PortDigitalIOS, mask=0xFF) -class PortDIOSet(RegisterBase[PortDIOSetPayload]): +class PortDIOSet(RegisterBase[PortDigitalIOS]): address: ClassVar[int] = 41 payload_type: ClassVar[PayloadType] = PayloadType.U8 payload_class = PortDIOSetPayload @@ -324,11 +323,11 @@ class StartPulseTrain(RegisterBase[StartPulseTrainPayload]): # =========================================================================== -class EncoderModePayload(StructPayload[np.uint8]): - Mode: EncoderModeMask = GroupMask(enum=EncoderModeMask, mask=0xFF) +class EncoderModePayload(AnonymousPayload[np.uint8]): + __value__: EncoderModeMask = GroupMask(enum=EncoderModeMask, mask=0xFF) -class EncoderMode(RegisterBase[EncoderModePayload]): +class EncoderMode(RegisterBase[EncoderModeMask]): address: ClassVar[int] = 103 payload_type: ClassVar[PayloadType] = PayloadType.U8 payload_class = EncoderModePayload @@ -409,8 +408,9 @@ def main() -> None: # pragma: no cover - manual exploration entry point assert int(_roundtrip(Counter0, np.int32(-100000))) == -100000 print("Counter0 OK") - p = _roundtrip(PortDIOSet, PortDIOSetPayload(DIO0=True, DIO3=True)) - assert p.DIO0 is True and p.DIO3 is True and p.DIO1 is False + p = _roundtrip(PortDIOSet, PortDigitalIOS.DIO0 | PortDigitalIOS.DIO3) + assert p == PortDigitalIOS.DIO0 | PortDigitalIOS.DIO3 # single-member unwrap + assert PortDigitalIOS.DIO1 not in p assert PortDIOSetPayload.dtype.itemsize == 1 print("PortDIOSet OK") @@ -439,8 +439,8 @@ def main() -> None: # pragma: no cover - manual exploration entry point assert int(StartPulseTrainPayload(PulseCount=np.uint8(3)).Frequency) == 1 # defaultValue print("StartPulseTrain OK (4 masked members, 2 words, default Frequency=1)") - p = _roundtrip(EncoderMode, EncoderModePayload(Mode=EncoderModeMask.Displacement)) - assert p.Mode == EncoderModeMask.Displacement + p = _roundtrip(EncoderMode, EncoderModeMask.Displacement) + assert p == EncoderModeMask.Displacement # single-member unwrap print("EncoderMode OK") print("\nAll device.yml registers round-trip cleanly.") diff --git a/tests/protocol/test_converter.py b/tests/protocol/test_converter.py index 70b559f..5fb675d 100644 --- a/tests/protocol/test_converter.py +++ b/tests/protocol/test_converter.py @@ -15,7 +15,7 @@ from harp.data import to_dataframe from harp.protocol._payload import ( PayloadBase, - BitFlag, + BitMask, Field, GroupMask, _IdentityConverter, @@ -34,6 +34,10 @@ class _Color(enum.IntEnum): Blue = 2 +class _Flag(enum.IntFlag): + A = 0x01 + + # --------------------------------------------------------------------------- # IdentityConverter — pass-through field # --------------------------------------------------------------------------- @@ -187,20 +191,20 @@ def test_bitfield_payloads_ndim_aware(): """Scalar records stay on the declared class; batches route to the auto-derived ``Batch`` twin.""" class _Flags(PayloadBase): - flag = BitFlag(mask=0x01) + flag = BitMask(enum=_Flag, mask=0x01) group = GroupMask(mask=0x06, enum=_Color) # 0-D scalar record: flag=1, group bits=01 (Green) scalar = _Flags.from_array(np.array((0x03,), dtype=_Flags.dtype)) assert type(scalar) is _Flags - assert scalar.flag is True + assert scalar.flag is _Flag.A assert scalar.group is _Color.Green # 1-D batch — Batch sibling, ndarray-typed accessors. batch = _Flags.from_buffer(bytes([0x01, 0x02])) assert type(batch) is _Flags.Batch assert isinstance(batch, _Flags) - np.testing.assert_array_equal(batch.flag, [True, False]) + np.testing.assert_array_equal(batch.flag, [1, 0]) np.testing.assert_array_equal(batch.group, [0, 1]) @@ -208,11 +212,11 @@ def test_bitfield_kwarg_init_round_trip(): """PayloadBase.__init__ supports bitfield kwargs with OR-into-slot encoding.""" class _Flags(PayloadBase): - flag = BitFlag(mask=0x01) + flag = BitMask(enum=_Flag, mask=0x01) group = GroupMask(mask=0x06, enum=_Color) - p = _Flags(flag=True, group=_Color.Green) - assert p.flag is True + p = _Flags(flag=_Flag.A, group=_Color.Green) + assert p.flag is _Flag.A assert p.group is _Color.Green # Wire byte: flag bit + (Green << 1) = 0x01 | 0x02 = 0x03. Masked fields on one # element share a slot named after the first declared field ("flag"). diff --git a/tests/protocol/test_payload.py b/tests/protocol/test_payload.py index e7d56b9..36828ff 100644 --- a/tests/protocol/test_payload.py +++ b/tests/protocol/test_payload.py @@ -13,7 +13,9 @@ class SimplePayload(PayloadBase): class BitPackedPayload(PayloadBase): packed = Field(converter=_IdentityConverter("u1")) - def to_columns(self, *, decode_enums: bool = True) -> list[Column]: + def to_columns( + self, *, decode_enums: bool = True, demux_bit_masks: bool = False + ) -> list[Column]: return [ Column("flag_a", (self.raw_payload["packed"] & 0x01).astype(bool)), Column("flag_b", ((self.raw_payload["packed"] >> 1) & 0x01).astype(bool)), diff --git a/tests/protocol/test_register.py b/tests/protocol/test_register.py index 3105501..6bd0164 100644 --- a/tests/protocol/test_register.py +++ b/tests/protocol/test_register.py @@ -352,24 +352,26 @@ def test_structured_payload_descriptors_multi(): def test_anonymous_payload_converter_roundtrip(): - """AnonymousPayload with a converter= encodes/decodes the single slot. + """A ``__value__`` Field codec encodes/decodes the single slot. Models a register that carries one value but needs a domain codec (e.g. DeviceName -> StringConverter). """ - from harp.protocol._payload import AnonymousPayload + from harp.protocol._payload import AnonymousPayload, Field from harp.protocol._payload_converters import StringConverter - class PayloadDeviceName(AnonymousPayload, converter=StringConverter(25)): - pass + class PayloadDeviceName(AnonymousPayload[np.uint8]): + __value__: str = Field(StringConverter(25)) class DeviceName(RegisterBase): address: ClassVar[int] = 12 payload_type: ClassVar[PayloadType] = PayloadType.U8 payload_class = PayloadDeviceName - # dtype derives from the converter; raw bytes are the encoded, null-padded value. - assert PayloadDeviceName.dtype == np.dtype((np.uint8, (25,))) + # dtype derives from the converter (one structured slot); raw bytes are the + # encoded, null-padded value. + assert PayloadDeviceName.dtype.names == ("__value__",) + assert PayloadDeviceName.dtype.itemsize == 25 payload = PayloadDeviceName("Behavior") assert payload.raw_payload.tobytes() == b"Behavior".ljust(25, b"\x00") @@ -390,10 +392,10 @@ class DeviceName(RegisterBase): def test_anonymous_payload_scalar_converter_roundtrip(): - """A scalar (non-sub-array) converter= also round-trips through unwrap.""" + """A scalar (non-sub-array) ``__value__`` codec also round-trips through unwrap.""" import enum - from harp.protocol._payload import AnonymousPayload + from harp.protocol._payload import AnonymousPayload, Field from harp.protocol._payload_converters import EnumConverter class Color(enum.IntEnum): @@ -401,10 +403,10 @@ class Color(enum.IntEnum): GREEN = 1 BLUE = 2 - class PayloadColor(AnonymousPayload, converter=EnumConverter(Color)): - pass + class PayloadColor(AnonymousPayload[np.uint8]): + __value__: Color = Field(EnumConverter(Color)) - assert PayloadColor.dtype == np.dtype(np.uint8) + assert PayloadColor.dtype.itemsize == 1 raw = PayloadColor(Color.BLUE).raw_payload.tobytes() record = np.frombuffer(raw, dtype=PayloadColor.dtype, count=1)[0] assert PayloadColor.unwrap(record) == Color.BLUE diff --git a/tests/protocol/test_register_modeling.py b/tests/protocol/test_register_modeling.py index 006e20c..98a471d 100644 --- a/tests/protocol/test_register_modeling.py +++ b/tests/protocol/test_register_modeling.py @@ -22,9 +22,9 @@ DigitalInputs, EncoderMode, EncoderModeMask, - EncoderModePayload, PortDIOSet, PortDIOSetPayload, + PortDigitalIOS, PulseDO0, PulseDOPort0, PwmPort, @@ -89,9 +89,10 @@ def test_custom_member_converter_roundtrip(): def test_encoder_mode_roundtrip(): - p = _roundtrip(EncoderMode, EncoderModePayload(Mode=EncoderModeMask.Displacement)) - assert p.Mode == EncoderModeMask.Displacement - assert isinstance(p.Mode, EncoderModeMask) + # Single whole-register groupMask -> parse() unwraps to the bare enum. + p = _roundtrip(EncoderMode, EncoderModeMask.Displacement) + assert p == EncoderModeMask.Displacement + assert isinstance(p, EncoderModeMask) # --------------------------------------------------------------------------- @@ -162,9 +163,11 @@ def test_bitmask_splitter_masked_ints(): assert p.raw_payload.tobytes() == bytes([0x5A]) # High packs into the top nibble -def test_port_dio_set_bitflags(): - p = _roundtrip(PortDIOSet, PortDIOSetPayload(DIO0=True, DIO3=True)) - assert p.DIO0 is True and p.DIO3 is True and p.DIO1 is False +def test_port_dio_set_bitmask(): + # Single whole-register bitMask -> parse() unwraps to the bare IntFlag. + p = _roundtrip(PortDIOSet, PortDigitalIOS.DIO0 | PortDigitalIOS.DIO3) + assert p == PortDigitalIOS.DIO0 | PortDigitalIOS.DIO3 + assert PortDigitalIOS.DIO1 not in p assert PortDIOSetPayload.dtype.itemsize == 1 @@ -174,10 +177,12 @@ def test_port_dio_set_bitflags(): def test_custom_payload_single_member_unwrap(): - assert CustomPayloadPayload._single_member == "value" + # Root payload: single __value__ view, unwrapped on parse. + assert CustomPayloadPayload._single_member == "__value__" + assert CustomPayloadPayload._root is True # U32[3] HarpVersion -> 12-byte buffer (3 x u32), same converter class. assert CustomPayloadPayload.dtype.itemsize == 12 - parsed = _roundtrip(CustomPayload, CustomPayloadPayload(value=HarpVersion(3, 1, 4))) + parsed = _roundtrip(CustomPayload, CustomPayloadPayload(HarpVersion(3, 1, 4))) assert isinstance(parsed, HarpVersion) assert parsed == HarpVersion(3, 1, 4) diff --git a/tests/test_device.py b/tests/test_device.py index 29cf401..037790f 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -1,5 +1,6 @@ """Tests for the device layer: descriptors, to_dataframe, and read_frames.""" +import enum from typing import ClassVar import numpy as np @@ -7,7 +8,7 @@ from harp.data import to_dataframe from harp.protocol._builder import build_message_frame from harp.protocol._message_type import MessageType -from harp.protocol._payload import PayloadBase, BitFlag, GroupMask +from harp.protocol._payload import PayloadBase, BitMask, GroupMask from harp.protocol._payload_type import PayloadType from harp.device._registers import ( @@ -18,15 +19,19 @@ ) +class Pins(enum.IntFlag): + PIN0 = 0x01 + PIN1 = 0x02 + PIN2 = 0x04 + PIN3 = 0x08 + PIN4 = 0x10 + PIN5 = 0x20 + PIN6 = 0x40 + PIN7 = 0x80 + + class PinsPayload(PayloadBase[np.uint8]): - pin0 = BitFlag(mask=0x01) - pin1 = BitFlag(mask=0x02) - pin2 = BitFlag(mask=0x04) - pin3 = BitFlag(mask=0x08) - pin4 = BitFlag(mask=0x10) - pin5 = BitFlag(mask=0x20) - pin6 = BitFlag(mask=0x40) - pin7 = BitFlag(mask=0x80) + pins = BitMask(enum=Pins, mask=0xFF) # --- Minimal fixture payload class ------------------------------------------ @@ -36,30 +41,30 @@ class _FlagPayload(PayloadBase[np.uint8]): _dtype: ClassVar = np.dtype("u1") _repr_fields: ClassVar = ("flag", "group") - flag = BitFlag(mask=0x01) + flag = BitMask(enum=Pins, mask=0x01) group = GroupMask(mask=0x06, enum=OperationMode) -# --- BitFlag behaviour ------------------------------------------------------ +# --- BitMask behaviour ------------------------------------------------------ -def test_bitflag_single_returns_bool_true(): +def test_bitmask_single_returns_flag_set(): p = _FlagPayload.from_buffer(bytes([0x01])) - assert p.flag is True - assert type(p.flag) is bool + assert p.flag == Pins.PIN0 + assert isinstance(p.flag, Pins) -def test_bitflag_single_returns_bool_false(): +def test_bitmask_single_returns_empty_flag(): p = _FlagPayload.from_buffer(bytes([0x00])) - assert p.flag is False - assert type(p.flag) is bool + assert p.flag == Pins(0) + assert isinstance(p.flag, Pins) -def test_bitflag_batch_returns_ndarray(): +def test_bitmask_batch_returns_raw_int_ndarray(): p = _FlagPayload.from_buffer(bytes([0x01, 0x00, 0x01])) result = p.flag assert isinstance(result, np.ndarray) - np.testing.assert_array_equal(result, [True, False, True]) + np.testing.assert_array_equal(result, [1, 0, 1]) # --- GroupMask behaviour ----------------------------------------------------- @@ -140,25 +145,34 @@ def test_op_ctrl_to_dataframe(): def test_pins_single_scalar(): p = PinsPayload.from_buffer(bytes([0b00000101])) - assert p.pin0 is True - assert p.pin1 is False - assert p.pin2 is True - assert p.pin7 is False + assert Pins.PIN0 in p.pins + assert Pins.PIN1 not in p.pins + assert Pins.PIN2 in p.pins + assert Pins.PIN7 not in p.pins -def test_pins_batch_ndarray(): +def test_pins_batch_raw_int(): p = PinsPayload.from_buffer(bytes([0b00000001, 0b00000010])) - np.testing.assert_array_equal(p.pin0, [True, False]) - np.testing.assert_array_equal(p.pin1, [False, True]) + np.testing.assert_array_equal(p.pins, [0b01, 0b10]) -def test_pins_to_dataframe(): +def test_pins_to_dataframe_single_column(): p = PinsPayload.from_buffer(bytes([0b00000101, 0b00000010])) df = to_dataframe(p) - assert list(df.columns) == list(PinsPayload._repr_fields) + assert list(df.columns) == ["pins"] + np.testing.assert_array_equal(df["pins"], [0b101, 0b10]) assert len(df) == 2 +def test_pins_to_dataframe_demuxed(): + p = PinsPayload.from_buffer(bytes([0b00000101, 0b00000010])) + df = to_dataframe(p, demux_bit_masks=True) + assert list(df.columns) == [m.name for m in Pins] + np.testing.assert_array_equal(df["PIN0"], [True, False]) + np.testing.assert_array_equal(df["PIN1"], [False, True]) + np.testing.assert_array_equal(df["PIN2"], [True, False]) + + # --- read_frames round-trip -------------------------------------------------- From d97fab961db91409a92d9c6214154564b73a97bd Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sun, 28 Jun 2026 14:04:09 -0700 Subject: [PATCH 243/267] Remove the use of deferred annotations --- tests/protocol/register_models.py | 11 ++++------- tests/protocol/test_register_modeling.py | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/protocol/register_models.py b/tests/protocol/register_models.py index e09e1eb..52740eb 100644 --- a/tests/protocol/register_models.py +++ b/tests/protocol/register_models.py @@ -19,12 +19,9 @@ * Masked sub-fields use ``GroupMask`` (enum) or ``Field(converter=..., mask=...)`` (numeric); the right-shift is derived from the mask's trailing zeros. * The register ``length`` (base elements) fixes ``itemsize`` so byte gaps survive. -* Enum decoding is strict (an out-of-range code raises) — a deliberate divergence - from the C# generator's unchecked cast. +* Enum decoding is strict: an out-of-range code raises. """ -from __future__ import annotations - import enum from typing import Any, ClassVar @@ -151,7 +148,7 @@ class AnalogData(RegisterBase[AnalogDataPayload]): class ComplexConfigurationPayload(StructPayload[np.uint8], length=17): - PwmPort: PwmPort = GroupMask(enum=PwmPort, mask=0xFF, offset=0) + PwmPort: "PwmPort" = GroupMask(enum=PwmPort, mask=0xFF, offset=0) # quoted: member name shadows enum type DutyCycle: np.float32 = Field(IdentityConverter(np.float32), offset=4) Frequency: np.float32 = Field(IdentityConverter(np.float32), offset=8) EventsEnabled: bool = Field(BoolConverter(), offset=12) @@ -254,8 +251,8 @@ class Counter0(RegisterS32): # =========================================================================== # 41 PortDIOSet : U8, Write — bitMask PortDigitalIOS. A single BitMask over the -# whole byte; bits >= 0x100 can't fit a U8 (and the C# generator's byte cast -# drops them too). Single-member -> parse() unwraps to a bare PortDigitalIOS. +# whole byte; bits >= 0x100 can't fit a U8 so they are dropped. Single-member +# -> parse() unwraps to a bare PortDigitalIOS. # =========================================================================== diff --git a/tests/protocol/test_register_modeling.py b/tests/protocol/test_register_modeling.py index 98a471d..675abeb 100644 --- a/tests/protocol/test_register_modeling.py +++ b/tests/protocol/test_register_modeling.py @@ -188,7 +188,7 @@ def test_custom_payload_single_member_unwrap(): # --------------------------------------------------------------------------- -# Strict enums: an out-of-range masked code raises (divergence from C#) +# Strict enums: an out-of-range masked code raises # --------------------------------------------------------------------------- From 755f9111ca4670a4355d2767f07bfe9abd11ef09 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sun, 28 Jun 2026 14:07:08 -0700 Subject: [PATCH 244/267] Linting --- scripts/core_registers_example.py | 5 ++++- src/packages/harp-data/src/harp/data/_reader.py | 5 +++-- src/packages/harp-protocol/src/harp/protocol/_payload.py | 2 +- tests/protocol/register_models.py | 4 +++- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/scripts/core_registers_example.py b/scripts/core_registers_example.py index 4669dfa..535124f 100644 --- a/scripts/core_registers_example.py +++ b/scripts/core_registers_example.py @@ -53,7 +53,10 @@ ), (ResetDevice, ResetFlags.RESTORE_DEFAULT | ResetFlags.RESTORE_NAME), (DeviceName, "my-harp-device"), - (ClockConfiguration, ClockConfigurationFlags.CLOCK_REPEATER | ClockConfigurationFlags.CLOCK_UNLOCK), + ( + ClockConfiguration, + ClockConfigurationFlags.CLOCK_REPEATER | ClockConfigurationFlags.CLOCK_UNLOCK, + ), (HardwareVersionHigh, np.uint8(2)), (HardwareVersionLow, np.uint8(0)), (AssemblyVersion, np.uint8(3)), diff --git a/src/packages/harp-data/src/harp/data/_reader.py b/src/packages/harp-data/src/harp/data/_reader.py index 98a27f7..39ea63a 100644 --- a/src/packages/harp-data/src/harp/data/_reader.py +++ b/src/packages/harp-data/src/harp/data/_reader.py @@ -19,6 +19,7 @@ def _read_bytes(source: Source) -> bytes: return source.read() return Path(source).read_bytes() + _DEFAULT_COLUMN_NAME = "value" @@ -49,8 +50,8 @@ def to_dataframe( expands each flag (``BitMask``) column into one boolean column per flag member. """ # TODO: we may need to account for cases where columns have the same name. - # this can happen when demuxing bitmasks, for example, where each bitmask column - # is expanded into multiple boolean columns with the same name. + # this can happen when demuxing bitmasks, for example, where each bitmask column + # is expanded into multiple boolean columns with the same name. return columns_to_dataframe( payload.to_columns(decode_enums=decode_enums, demux_bit_masks=demux_bit_masks) ) diff --git a/src/packages/harp-protocol/src/harp/protocol/_payload.py b/src/packages/harp-protocol/src/harp/protocol/_payload.py index 55fcc6b..5e06359 100644 --- a/src/packages/harp-protocol/src/harp/protocol/_payload.py +++ b/src/packages/harp-protocol/src/harp/protocol/_payload.py @@ -897,7 +897,7 @@ class PayloadU16(AnonymousPayload, scalar_dtype=" Date: Sun, 28 Jun 2026 15:56:52 -0700 Subject: [PATCH 245/267] Fix example --- scripts/core_registers_example.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scripts/core_registers_example.py b/scripts/core_registers_example.py index 535124f..3d35f19 100644 --- a/scripts/core_registers_example.py +++ b/scripts/core_registers_example.py @@ -49,6 +49,7 @@ visual_indicators=EnableFlag.ENABLED, operation_led=EnableFlag.ENABLED, heartbeat=EnableFlag.ENABLED, + mute_replies=True, ), ), (ResetDevice, ResetFlags.RESTORE_DEFAULT | ResetFlags.RESTORE_NAME), @@ -81,11 +82,16 @@ operation_mode=OperationMode.ACTIVE, heartbeat=EnableFlag.ENABLED, visual_indicators=EnableFlag.ENABLED, + dump_registers=False, + mute_replies=False, + operation_led=EnableFlag.DISABLED, ) print(f" operation_mode : {ctrl.operation_mode}") # OperationMode.ACTIVE print(f" heartbeat : {ctrl.heartbeat}") # EnableFlag.ENABLED print(f" dump_registers : {ctrl.dump_registers}") # False print(f" visual_indicators : {ctrl.visual_indicators}") # EnableFlag.ENABLED +print(f" mute_replies : {ctrl.mute_replies}") # False +print(f" operation_led : {ctrl.operation_led}") # EnableFlag.DISABLED # --------------------------------------------------------------------------- # Bulk read from a .bin file (zero-copy, vectorised) @@ -99,6 +105,10 @@ print(f" timestamps (s) : {timestamps}") print(f" operation_mode : {payload.operation_mode}") print(f" heartbeat : {payload.heartbeat}") +print(f" dump_registers : {payload.dump_registers}") +print(f" visual_indicators : {payload.visual_indicators}") +print(f" mute_replies : {payload.mute_replies}") +print(f" operation_led : {payload.operation_led}") print("\n DataFrame:") df = to_dataframe(payload) From 5716d5352637e0844212c4b33d4c52e599bc7571 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sun, 28 Jun 2026 16:05:40 -0700 Subject: [PATCH 246/267] Refactor method's names --- .../read_data_to_dataframe.py | 6 +-- scripts/benchmark_analog_data.py | 6 +-- scripts/core_registers_example.py | 4 +- .../harp-data/src/harp/data/__init__.py | 7 ++-- .../harp-data/src/harp/data/_reader.py | 38 +++++++------------ tests/protocol/test_converter.py | 4 +- tests/protocol/test_payload.py | 6 +-- tests/protocol/test_register.py | 8 ++-- tests/protocol/test_register_modeling.py | 4 +- tests/test_device.py | 10 ++--- 10 files changed, 41 insertions(+), 52 deletions(-) diff --git a/docs/examples/read_data_to_dataframe/read_data_to_dataframe.py b/docs/examples/read_data_to_dataframe/read_data_to_dataframe.py index 93a56af..6f1459c 100644 --- a/docs/examples/read_data_to_dataframe/read_data_to_dataframe.py +++ b/docs/examples/read_data_to_dataframe/read_data_to_dataframe.py @@ -1,11 +1,11 @@ -from harp.data import read_dataframe +from harp.data import parse_to_dataframe from harp.device import OperationControl # Parse a register's binary dump into a pandas DataFrame — one row per frame, # one column per field, plus a leading "timestamp" column. -df = read_dataframe(OperationControl, "OperationControl.bin", timestamp=True) +df = parse_to_dataframe(OperationControl, "OperationControl.bin", timestamp=True) print(df.head()) # `read_dataframe` also accepts raw bytes or an open binary file object: with open("OperationControl.bin", "rb") as f: - df = read_dataframe(OperationControl, f) + df = parse_to_dataframe(OperationControl, f) diff --git a/scripts/benchmark_analog_data.py b/scripts/benchmark_analog_data.py index 06d1450..3a563eb 100644 --- a/scripts/benchmark_analog_data.py +++ b/scripts/benchmark_analog_data.py @@ -31,7 +31,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent)) from scripts._harp_io import read as harp_read # vendored harp-python read() -from harp.data import read_dataframe, to_dataframe +from harp.data import parse_to_dataframe, payload_to_dataframe from harp.protocol import PayloadBase, Field, PayloadType, RegisterBase, IdentityConverter @@ -71,7 +71,7 @@ def pyharp_read(path: Path, *, include_timestamp: bool = True): """pyharp path: parse_bulk → to_dataframe.""" bytes_2_parse = path.read_bytes() _data, timestamps, msg_type, payload = AnalogData.parse_bulk(bytes_2_parse) - df = to_dataframe(payload) + df = payload_to_dataframe(payload) if include_timestamp: df.insert(0, "timestamp", timestamps) return df @@ -79,7 +79,7 @@ def pyharp_read(path: Path, *, include_timestamp: bool = True): def pyharp_read_dataframe(path: Path, *, timestamp: bool = True): """pyharp one-call path: read_dataframe.""" - return read_dataframe(AnalogData, path.read_bytes(), timestamp=timestamp) + return parse_to_dataframe(AnalogData, path.read_bytes(), timestamp=timestamp) def harp_python_read(path: Path): diff --git a/scripts/core_registers_example.py b/scripts/core_registers_example.py index 3d35f19..e8fbd8d 100644 --- a/scripts/core_registers_example.py +++ b/scripts/core_registers_example.py @@ -34,7 +34,7 @@ WhoAmI, ) -from harp.data import to_dataframe +from harp.data import payload_to_dataframe from harp.protocol import HarpMessage examples = [ @@ -111,6 +111,6 @@ print(f" operation_led : {payload.operation_led}") print("\n DataFrame:") -df = to_dataframe(payload) +df = payload_to_dataframe(payload) df.insert(0, "timestamp", timestamps) print(df.to_string(index=False)) diff --git a/src/packages/harp-data/src/harp/data/__init__.py b/src/packages/harp-data/src/harp/data/__init__.py index 9aa9826..c8208f1 100644 --- a/src/packages/harp-data/src/harp/data/__init__.py +++ b/src/packages/harp-data/src/harp/data/__init__.py @@ -1,7 +1,6 @@ -from ._reader import columns_to_dataframe, read_dataframe, to_dataframe +from ._reader import parse_to_dataframe, payload_to_dataframe __all__ = [ - "read_dataframe", - "to_dataframe", - "columns_to_dataframe", + "parse_to_dataframe", + "payload_to_dataframe", ] diff --git a/src/packages/harp-data/src/harp/data/_reader.py b/src/packages/harp-data/src/harp/data/_reader.py index 39ea63a..a9a7251 100644 --- a/src/packages/harp-data/src/harp/data/_reader.py +++ b/src/packages/harp-data/src/harp/data/_reader.py @@ -5,7 +5,7 @@ import numpy as np import pandas as pd -from harp.protocol import Column, RegisterBase +from harp.protocol import RegisterBase Source = Union[str, Path, bytes, bytearray, memoryview, BinaryIO] @@ -23,25 +23,7 @@ def _read_bytes(source: Source) -> bytes: _DEFAULT_COLUMN_NAME = "value" -def columns_to_dataframe(columns: list[Column]) -> pd.DataFrame: - """Assemble :class:`~harp.protocol.Column` objects into a DataFrame. - - Enum-backed columns (``categories`` set) become ``pd.Categorical`` built - from codes — no string materialization; everything else is used as-is. - """ - return pd.DataFrame( - { - (c.name if c.name is not None else _DEFAULT_COLUMN_NAME): ( - pd.Categorical.from_codes(c.data, categories=c.categories) - if c.categories is not None - else c.data - ) - for c in columns - } - ) - - -def to_dataframe( +def payload_to_dataframe( payload: Any, *, decode_enums: bool = True, demux_bit_masks: bool = False ) -> pd.DataFrame: """Turn a (batched) payload into a DataFrame, one row per frame. @@ -52,12 +34,20 @@ def to_dataframe( # TODO: we may need to account for cases where columns have the same name. # this can happen when demuxing bitmasks, for example, where each bitmask column # is expanded into multiple boolean columns with the same name. - return columns_to_dataframe( - payload.to_columns(decode_enums=decode_enums, demux_bit_masks=demux_bit_masks) + cols = payload.to_columns(decode_enums=decode_enums, demux_bit_masks=demux_bit_masks) + return pd.DataFrame( + { + (c.name if c.name is not None else _DEFAULT_COLUMN_NAME): ( + pd.Categorical.from_codes(c.data, categories=c.categories) + if c.categories is not None + else c.data + ) + for c in cols + } ) -def read_dataframe( +def parse_to_dataframe( register: type[RegisterBase[Any]], source: Source, *, @@ -76,7 +66,7 @@ def read_dataframe( """ raw = _read_bytes(source) _data, timestamps, msg_view, payload = register.parse_bulk(raw, parse_timestamp=timestamp) - df = to_dataframe(payload, decode_enums=decode_enums, demux_bit_masks=demux_bit_masks) + df = payload_to_dataframe(payload, decode_enums=decode_enums, demux_bit_masks=demux_bit_masks) if message_type and msg_view is not None: df.insert( diff --git a/tests/protocol/test_converter.py b/tests/protocol/test_converter.py index 5fb675d..901095d 100644 --- a/tests/protocol/test_converter.py +++ b/tests/protocol/test_converter.py @@ -12,7 +12,7 @@ import numpy as np import pytest -from harp.data import to_dataframe +from harp.data import payload_to_dataframe from harp.protocol._payload import ( PayloadBase, BitMask, @@ -126,7 +126,7 @@ def test_string_converter_to_dataframe(): rec1 = _NamedPayload(name="hi", delta=1).raw_payload.tobytes() rec2 = _NamedPayload(name="bye", delta=2).raw_payload.tobytes() batch = _NamedPayload.from_buffer(rec1 + rec2) - df = to_dataframe(batch) + df = payload_to_dataframe(batch) # Non-identity converter produces one column per field — no sub-array # expansion for the string field. assert list(df.columns) == ["name", "delta"] diff --git a/tests/protocol/test_payload.py b/tests/protocol/test_payload.py index 36828ff..4444ec1 100644 --- a/tests/protocol/test_payload.py +++ b/tests/protocol/test_payload.py @@ -1,6 +1,6 @@ import numpy as np import pytest -from harp.data import to_dataframe +from harp.data import payload_to_dataframe from harp.protocol import Column from harp.protocol._payload import PayloadBase, Field, _IdentityConverter @@ -44,7 +44,7 @@ def test_from_buffer_values(): def test_to_dataframe_columns(): p = SimplePayload.from_buffer(_make_simple_bytes(3)) - df = to_dataframe(p) + df = payload_to_dataframe(p) assert list(df.columns) == ["x", "y"] assert len(df) == 3 @@ -52,7 +52,7 @@ def test_to_dataframe_columns(): def test_to_dataframe_override(): arr = np.array([(0b00000011,), (0b00000001,), (0b00000010,)], dtype=BitPackedPayload.dtype) p = BitPackedPayload.from_buffer(arr.tobytes()) - df = to_dataframe(p) + df = payload_to_dataframe(p) assert list(df.columns) == ["flag_a", "flag_b"] assert list(df["flag_a"]) == [True, True, False] assert list(df["flag_b"]) == [True, False, True] diff --git a/tests/protocol/test_register.py b/tests/protocol/test_register.py index 6bd0164..be165e4 100644 --- a/tests/protocol/test_register.py +++ b/tests/protocol/test_register.py @@ -4,7 +4,7 @@ import numpy as np import pytest -from harp.data import to_dataframe +from harp.data import payload_to_dataframe from harp.protocol._message import HarpMessage from harp.protocol._message_type import MessageType from harp.protocol._payload import ( @@ -205,7 +205,7 @@ def test_structured_register_to_dataframe(): ).tobytes() # Bulk decode goes through .Batch; from_buffer handles the redirect. bulk = AnalogDataPayload.from_buffer(raw) - df = to_dataframe(bulk) + df = payload_to_dataframe(bulk) assert list(df.columns) == ["analog_input0", "encoder", "analog_input1"] assert len(df) == 2 assert df["analog_input0"].tolist() == [1, 4] @@ -382,13 +382,13 @@ class DeviceName(RegisterBase): assert parsed == "Behavior" # to_dataframe decodes both a single record and a batch. - assert to_dataframe(PayloadDeviceName("Behavior"))["value"].tolist() == ["Behavior"] + assert payload_to_dataframe(PayloadDeviceName("Behavior"))["value"].tolist() == ["Behavior"] two = ( PayloadDeviceName("Foo").raw_payload.tobytes() + PayloadDeviceName("Bar").raw_payload.tobytes() ) batch = PayloadDeviceName.from_buffer(two) - assert to_dataframe(batch)["value"].tolist() == ["Foo", "Bar"] + assert payload_to_dataframe(batch)["value"].tolist() == ["Foo", "Bar"] def test_anonymous_payload_scalar_converter_roundtrip(): diff --git a/tests/protocol/test_register_modeling.py b/tests/protocol/test_register_modeling.py index 675abeb..186c7c7 100644 --- a/tests/protocol/test_register_modeling.py +++ b/tests/protocol/test_register_modeling.py @@ -5,7 +5,7 @@ import numpy as np import pytest -from harp.data import to_dataframe +from harp.data import payload_to_dataframe from harp.protocol import HarpMessage, HarpVersion from tests.protocol.register_models import ( AnalogData, @@ -214,7 +214,7 @@ def test_complex_configuration_to_dataframe(): Delta=np.uint32(42), ) batch = ComplexConfigurationPayload.from_buffer(cc.raw_payload.tobytes() * 2) - df = to_dataframe(batch) + df = payload_to_dataframe(batch) assert len(df) == 2 assert list(df["PwmPort"]) == ["Pwm2", "Pwm2"] np.testing.assert_array_equal(df["Delta"], [42, 42]) diff --git a/tests/test_device.py b/tests/test_device.py index 037790f..b49452d 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -5,7 +5,7 @@ import numpy as np -from harp.data import to_dataframe +from harp.data import payload_to_dataframe from harp.protocol._builder import build_message_frame from harp.protocol._message_type import MessageType from harp.protocol._payload import PayloadBase, BitMask, GroupMask @@ -130,7 +130,7 @@ def test_op_ctrl_to_dataframe(): _make_op_ctrl_byte(OperationMode.STANDBY, EnableFlag.DISABLED), ] p = OperationControlPayload.from_buffer(bytes(vals)) - df = to_dataframe(p, decode_enums=False) + df = payload_to_dataframe(p, decode_enums=False) assert list(df.columns) == list(OperationControlPayload._repr_fields) assert len(df) == 2 np.testing.assert_array_equal(df["heartbeat"], [True, False]) @@ -158,7 +158,7 @@ def test_pins_batch_raw_int(): def test_pins_to_dataframe_single_column(): p = PinsPayload.from_buffer(bytes([0b00000101, 0b00000010])) - df = to_dataframe(p) + df = payload_to_dataframe(p) assert list(df.columns) == ["pins"] np.testing.assert_array_equal(df["pins"], [0b101, 0b10]) assert len(df) == 2 @@ -166,7 +166,7 @@ def test_pins_to_dataframe_single_column(): def test_pins_to_dataframe_demuxed(): p = PinsPayload.from_buffer(bytes([0b00000101, 0b00000010])) - df = to_dataframe(p, demux_bit_masks=True) + df = payload_to_dataframe(p, demux_bit_masks=True) assert list(df.columns) == [m.name for m in Pins] np.testing.assert_array_equal(df["PIN0"], [True, False]) np.testing.assert_array_equal(df["PIN1"], [False, True]) @@ -235,7 +235,7 @@ def test_read_frames_to_dataframe(): vals = [_make_op_ctrl_byte(OperationMode.ACTIVE), _make_op_ctrl_byte(), _make_op_ctrl_byte()] raw = _make_frames(vals) _, payload = _read_frames(raw) - df = to_dataframe(payload) + df = payload_to_dataframe(payload) assert len(df) == 3 assert "heartbeat" in df.columns assert "operation_mode" in df.columns From 9231b27ee91e82c2d52e9cd67c97e4ddd1a38128 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Mon, 29 Jun 2026 08:16:01 -0700 Subject: [PATCH 247/267] Do not copy when converting to dataframe by default --- src/packages/harp-data/src/harp/data/_reader.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/packages/harp-data/src/harp/data/_reader.py b/src/packages/harp-data/src/harp/data/_reader.py index a9a7251..aea02a5 100644 --- a/src/packages/harp-data/src/harp/data/_reader.py +++ b/src/packages/harp-data/src/harp/data/_reader.py @@ -24,7 +24,7 @@ def _read_bytes(source: Source) -> bytes: def payload_to_dataframe( - payload: Any, *, decode_enums: bool = True, demux_bit_masks: bool = False + payload: Any, *, decode_enums: bool = True, demux_bit_masks: bool = False, copy: bool = False ) -> pd.DataFrame: """Turn a (batched) payload into a DataFrame, one row per frame. @@ -43,7 +43,8 @@ def payload_to_dataframe( else c.data ) for c in cols - } + }, + copy=copy, ) From 6bbe246f5321e6511b8c4c8b4d814e27b58630e7 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Wed, 1 Jul 2026 08:07:26 -0700 Subject: [PATCH 248/267] Add benchmark package --- .gitignore | 4 +- pyproject.toml | 2 + src/packages/harp-benchmarks/README.md | 57 +++ src/packages/harp-benchmarks/pyproject.toml | 27 ++ .../src/harp/benchmarks/__init__.py | 0 .../src/harp/benchmarks/_registers.py | 145 ++++++++ .../src/harp/benchmarks/benchmark.py | 333 ++++++++++++++++++ .../src/harp/benchmarks/generate.py | 92 +++++ .../src/harp/benchmarks}/register_models.py | 32 +- tests/protocol/test_register_modeling.py | 4 +- uv.lock | 22 ++ 11 files changed, 689 insertions(+), 29 deletions(-) create mode 100644 src/packages/harp-benchmarks/README.md create mode 100644 src/packages/harp-benchmarks/pyproject.toml create mode 100644 src/packages/harp-benchmarks/src/harp/benchmarks/__init__.py create mode 100644 src/packages/harp-benchmarks/src/harp/benchmarks/_registers.py create mode 100644 src/packages/harp-benchmarks/src/harp/benchmarks/benchmark.py create mode 100644 src/packages/harp-benchmarks/src/harp/benchmarks/generate.py rename {tests/protocol => src/packages/harp-benchmarks/src/harp/benchmarks}/register_models.py (91%) diff --git a/.gitignore b/.gitignore index f6cc12e..40bba35 100644 --- a/.gitignore +++ b/.gitignore @@ -174,4 +174,6 @@ cython_debug/ .pypirc # vscode -.vscode/ \ No newline at end of file +.vscode/ +# Internal benchmark artifacts (harp-benchmarks) +/benchmark/ diff --git a/pyproject.toml b/pyproject.toml index 046c529..550a1e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ harp-protocol = { workspace = true } harp-device = { workspace = true } harp-serial = { workspace = true } harp-data = { workspace = true } +harp-benchmarks = { workspace = true } [tool.uv.workspace] members = [ @@ -52,6 +53,7 @@ dev = [ "harp-device", "harp-serial", "harp-data", + "harp-benchmarks", "typing-extensions>=4.15.0", ] diff --git a/src/packages/harp-benchmarks/README.md b/src/packages/harp-benchmarks/README.md new file mode 100644 index 0000000..35a77c3 --- /dev/null +++ b/src/packages/harp-benchmarks/README.md @@ -0,0 +1,57 @@ +# harp-benchmarks + +**Internal** parsing-speed benchmarks for the Harp register/payload API, run over every +register defined in [`register_models.py`](src/harp/benchmarks/register_models.py) (the +device.yml coverage model — also imported by the acceptance tests under `tests/`). + +> This package is **not published to PyPI** (`classifiers = ["Private :: Do Not Upload"]` +> in its `pyproject.toml`). It is a workspace member consumed only in dev/internal builds +> via the repo-root `pyproject.toml` `dev` dependency-group. Its console scripts ship with +> this package, so they never leak into the published `harp` distribution. + +## Layout + +| Path | Purpose | +| --- | --- | +| `src/harp/benchmarks/register_models.py` | Reference models for every device.yml register (fixtures shared with the acceptance tests). | +| `src/harp/benchmarks/_registers.py` | Registry: each register + a representative sample value; artifact paths. | +| `src/harp/benchmarks/generate.py` | Writes `./benchmark/data/_.bin`; exposes `ensure_corpus` (cache-aware). | +| `src/harp/benchmarks/benchmark.py` | Ensures corpora exist, then times `parse_bulk`, `parse_to_dataframe`, `to_columns`; writes `./benchmark/report.md`. | + +All generated artifacts (corpora + report) are written under **`./benchmark`** in the +current working directory — git-ignored and fully regenerable. + +## Usage + +Console scripts are declared in this package's `pyproject.toml`: + +```bash +# One command: generate-if-needed (cache honored), then benchmark + write the report. +uv run harp-benchmark +uv run harp-benchmark --runs 20 +uv run harp-benchmark --entries 100000 --force # rebuild smaller corpora +uv run harp-benchmark --only Version ComplexConfiguration + +# Generate corpora explicitly (optional — harp-benchmark does this for you). +uv run harp-benchmark-generate +uv run harp-benchmark-generate --entries 100000 +``` + +Equivalent module invocations: `uv run python -m harp.benchmarks.benchmark` / +`uv run python -m harp.benchmarks.generate`. + +## What is measured + +- **`parse_bulk`** — the core zero-copy strided-view parse into a `Batch` payload. + This is **lazy**: it builds strided views only and runs **no** converters. +- **`parse_to_dataframe`** — the full path to a pandas `DataFrame` (`copy=False`). +- **`to_columns`** (decode only) — `parse_bulk` views built once up front, then only + `payload.to_columns()` timed. This is where each field's `converter.decode_batch` + actually runs, with no file read and no pandas construction. + +`parse_bulk` and `parse_to_dataframe` are each timed in two modes: + +- **pre-read** — file read once up front; only deserialization is timed (isolates library speed). +- **re-read** — file re-read from disk on every run (real-world "load a dump" path, includes disk). + +The report also decomposes `parse_to_dataframe ≈ parse_bulk + to_columns + pandas overhead`. diff --git a/src/packages/harp-benchmarks/pyproject.toml b/src/packages/harp-benchmarks/pyproject.toml new file mode 100644 index 0000000..96f54cb --- /dev/null +++ b/src/packages/harp-benchmarks/pyproject.toml @@ -0,0 +1,27 @@ +[project] +name = "harp-benchmarks" +version = "0.1.0" +description = "Internal parsing-speed benchmarks for the Harp register/payload API." +requires-python = ">=3.11" +# INTERNAL ONLY — never published to PyPI. The "Private :: Do Not Upload" trove +# classifier is rejected by PyPI (and twine/uv publish), so an accidental upload +# of this distribution fails fast. +classifiers = ["Private :: Do Not Upload"] +dependencies = [ + "harp-protocol", + "harp-data", + "numpy>=1.24", + "pandas>=2.0", +] + +[project.scripts] +harp-benchmark = "harp.benchmarks.benchmark:main" +harp-benchmark-generate = "harp.benchmarks.generate:main" + +[build-system] +requires = ["uv_build>=0.9.5,<0.10.0"] +build-backend = "uv_build" + +[tool.uv.build-backend] +module-name = "harp.benchmarks" +module-root = "src" diff --git a/src/packages/harp-benchmarks/src/harp/benchmarks/__init__.py b/src/packages/harp-benchmarks/src/harp/benchmarks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/packages/harp-benchmarks/src/harp/benchmarks/_registers.py b/src/packages/harp-benchmarks/src/harp/benchmarks/_registers.py new file mode 100644 index 0000000..808bb83 --- /dev/null +++ b/src/packages/harp-benchmarks/src/harp/benchmarks/_registers.py @@ -0,0 +1,145 @@ +"""Shared benchmark fixtures: every register from ``harp.benchmarks.register_models`` +paired with a representative sample value. + +Both ``generate.py`` (writes the .bin corpora) and ``benchmark.py`` (times parsing) +import :data:`BENCHMARK_REGISTERS` from here so the two stay in lock-step: the value +used to *format* each frame is the same one whose parsed shape we benchmark. + +The sample values mirror ``register_models.main()``'s round-trip fixtures — they +exercise the full spread of payload shapes the Harp protocol allows (trivial +scalars, struct payloads with byte gaps, masked sub-fields, custom converters, +enum/flag single-member unwrap). + +All generated artifacts live under ``./benchmark`` in the current working directory. +""" + +from pathlib import Path +from typing import Any, NamedTuple + +import numpy as np + +from harp.benchmarks.register_models import ( + AnalogData, + AnalogDataPayload, + BitmaskSplitter, + BitmaskSplitterPayload, + ComplexConfiguration, + ComplexConfigurationPayload, + Counter0, + CustomMemberConverter, + CustomMemberConverterPayload, + CustomPayload, + CustomRawPayload, + DigitalInputs, + EncoderMode, + EncoderModeMask, + PortDigitalIOS, + PortDIOSet, + PulseDO0, + PulseDOPort0, + PwmPort, + StartPulse, + StartPulsePayload, + StartPulseTrain, + StartPulseTrainPayload, + Version, + VersionPayload, +) +from harp.protocol import HarpVersion, RegisterBase + +ARTIFACTS_DIR = Path("benchmark").resolve() +DATA_DIR = ARTIFACTS_DIR / "data" +REPORT_PATH = ARTIFACTS_DIR / "report.md" + + +class BenchmarkedRegister(NamedTuple): + """A register under benchmark together with a value that ``format()`` accepts.""" + + name: str + register: type[RegisterBase[Any]] + value: Any + + @property + def address(self) -> int: + return self.register.address + + @property + def filename(self) -> str: + return f"{self.name}_{self.address}.bin" + + +def _build() -> list[BenchmarkedRegister]: + return [ + BenchmarkedRegister("DigitalInputs", DigitalInputs, np.uint8(0b1010)), + BenchmarkedRegister( + "AnalogData", + AnalogData, + AnalogDataPayload( + Analog0=np.float32(1.0), + Analog1=np.float32(2.0), + Analog2=np.float32(3.0), + Accelerometer=np.array([4, 5, 6], dtype=np.float32), + ), + ), + BenchmarkedRegister( + "ComplexConfiguration", + ComplexConfiguration, + ComplexConfigurationPayload( + PwmPort=PwmPort.Pwm2, + DutyCycle=np.float32(0.5), + Frequency=np.float32(1000.0), + EventsEnabled=True, + Delta=np.uint32(42), + ), + ), + BenchmarkedRegister( + "Version", + Version, + VersionPayload( + ProtocolVersion=HarpVersion(2, 0, 0), + FirmwareVersion=HarpVersion(1, 2, 3), + HardwareVersion=HarpVersion(1, 0, 0), + CoreId="abc", + InterfaceHash=np.arange(20, dtype=np.uint8), + ), + ), + BenchmarkedRegister("CustomPayload", CustomPayload, HarpVersion(3, 1, 4)), + BenchmarkedRegister("CustomRawPayload", CustomRawPayload, HarpVersion(0, 0, 1)), + BenchmarkedRegister( + "CustomMemberConverter", + CustomMemberConverter, + CustomMemberConverterPayload(Header=np.uint8(7), Data=-1234), + ), + BenchmarkedRegister( + "BitmaskSplitter", + BitmaskSplitter, + BitmaskSplitterPayload(Low=np.int32(0xA), High=np.int32(0x5)), + ), + BenchmarkedRegister("Counter0", Counter0, np.int32(-100000)), + BenchmarkedRegister( + "PortDIOSet", + PortDIOSet, + PortDigitalIOS.DIO0 | PortDigitalIOS.DIO3, + ), + BenchmarkedRegister("PulseDOPort0", PulseDOPort0, np.uint16(5)), + BenchmarkedRegister("PulseDO0", PulseDO0, np.uint16(9)), + BenchmarkedRegister( + "StartPulse", + StartPulse, + StartPulsePayload(DigitalOutput=PwmPort.Pwm1, PulseWidth=np.uint16(300)), + ), + BenchmarkedRegister( + "StartPulseTrain", + StartPulseTrain, + StartPulseTrainPayload( + DigitalOutput=PwmPort.Pwm1, + PulseWidth=np.uint16(300), + Frequency=np.uint8(200), + PulseCount=np.uint8(50), + ), + ), + BenchmarkedRegister("EncoderMode", EncoderMode, EncoderModeMask.Displacement), + ] + + +BENCHMARK_REGISTERS: list[BenchmarkedRegister] = _build() diff --git a/src/packages/harp-benchmarks/src/harp/benchmarks/benchmark.py b/src/packages/harp-benchmarks/src/harp/benchmarks/benchmark.py new file mode 100644 index 0000000..19f3b36 --- /dev/null +++ b/src/packages/harp-benchmarks/src/harp/benchmarks/benchmark.py @@ -0,0 +1,333 @@ +import argparse +import platform +import sys +from dataclasses import dataclass +from pathlib import Path +from statistics import mean, stdev +from time import perf_counter +from typing import Callable + +import numpy as np +import pandas as pd + +from harp.benchmarks._registers import ( + BENCHMARK_REGISTERS, + DATA_DIR, + REPORT_PATH, + BenchmarkedRegister, +) +from harp.benchmarks.generate import ensure_corpus +from harp.data import parse_to_dataframe + +_MIB = 1024**2 + + +@dataclass +class TimingStats: + """Per-run timings (seconds) and throughput derived from the mean.""" + + min: float + mean: float + max: float + stdev: float + frames: int + file_bytes: int + + @property + def mframes_per_s(self) -> float: + return (self.frames / self.mean) / 1e6 + + @property + def mib_per_s(self) -> float: + return (self.file_bytes / self.mean) / _MIB + + +def _time(fn: Callable[[], object], *, runs: int, frames: int, file_bytes: int) -> TimingStats: + fn() # warm-up (imports, caches, first-touch pages) — not measured + samples: list[float] = [] + for _ in range(runs): + t0 = perf_counter() + fn() + samples.append(perf_counter() - t0) + return TimingStats( + min=min(samples), + mean=mean(samples), + max=max(samples), + stdev=stdev(samples) if len(samples) > 1 else 0.0, + frames=frames, + file_bytes=file_bytes, + ) + + +@dataclass +class RegisterResult: + name: str + address: int + frames: int + stride: int + payload_bytes: int + file_bytes: int + bulk_preread: TimingStats + bulk_reread: TimingStats + cols: TimingStats + df_preread: TimingStats + df_reread: TimingStats + + +def _dataset_info(raw: bytes, payload_bytes: int) -> tuple[int, int]: + data = np.frombuffer(raw, dtype=np.uint8) + stride = int(data[1]) + 2 + nrows = len(data) // stride + return nrows, stride + + +def benchmark_register(reg: BenchmarkedRegister, path: Path, *, runs: int) -> RegisterResult: + register = reg.register + raw = path.read_bytes() + file_bytes = len(raw) + payload_bytes = register.payload_class.dtype.itemsize + frames, stride = _dataset_info(raw, payload_bytes) + + bulk_pre = _time( + lambda: register.parse_bulk(raw, parse_timestamp=True), + runs=runs, + frames=frames, + file_bytes=file_bytes, + ) + bulk_re = _time( + lambda: register.parse_bulk(path.read_bytes(), parse_timestamp=True), + runs=runs, + frames=frames, + file_bytes=file_bytes, + ) + # Decode only: pre-parse the bulk views once, then time to_columns() alone — + # this is where every converter's decode_batch runs, with no file read and no + # pandas DataFrame construction. Matches parse_to_dataframe's decode options. + _, _, _, payload = register.parse_bulk(raw, parse_timestamp=True) + cols = _time( + lambda: payload.to_columns(decode_enums=True, demux_bit_masks=False), + runs=runs, + frames=frames, + file_bytes=file_bytes, + ) + df_pre = _time( + lambda: parse_to_dataframe(register, raw, timestamp=True), + runs=runs, + frames=frames, + file_bytes=file_bytes, + ) + df_re = _time( + lambda: parse_to_dataframe(register, path.read_bytes(), timestamp=True), + runs=runs, + frames=frames, + file_bytes=file_bytes, + ) + + return RegisterResult( + name=reg.name, + address=reg.address, + frames=frames, + stride=stride, + payload_bytes=payload_bytes, + file_bytes=file_bytes, + bulk_preread=bulk_pre, + bulk_reread=bulk_re, + cols=cols, + df_preread=df_pre, + df_reread=df_re, + ) + + +def _fmt_ms(s: float) -> str: + return f"{s * 1e3:.2f}" + + +def build_report(results: list[RegisterResult], *, runs: int) -> str: + lines: list[str] = [] + lines.append("# Harp parsing benchmark\n") + lines.append( + "Parsing throughput for every register in `harp.benchmarks.register_models`, " + "measured over Harp wire-format dumps generated by `harp.benchmarks.generate`.\n" + ) + + # Environment + lines.append("## Environment\n") + lines.append("| Key | Value |") + lines.append("| --- | --- |") + lines.append(f"| Platform | {platform.platform()} |") + lines.append(f"| Python | {sys.version.split()[0]} |") + lines.append(f"| NumPy | {np.__version__} |") + lines.append(f"| pandas | {pd.__version__} |") + lines.append(f"| Runs per measurement | {runs} |") + lines.append( + "| Measured op | one full-file parse; min/mean/stdev over runs (1 warm-up discarded) |\n" + ) + + # Dataset summary + lines.append("## Datasets\n") + lines.append("| Register | Addr | Frames | Payload (B) | Frame/stride (B) | File (MiB) |") + lines.append("| --- | ---: | ---: | ---: | ---: | ---: |") + for r in results: + lines.append( + f"| {r.name} | {r.address} | {r.frames:,} | {r.payload_bytes} | " + f"{r.stride} | {r.file_bytes / _MIB:.1f} |" + ) + lines.append("") + + def _table( + title: str, + note: str, + pick: Callable[[RegisterResult], tuple[TimingStats, TimingStats]], + ) -> None: + lines.append(f"## {title}\n") + lines.append(f"{note}\n") + lines.append( + "| Register | Frames | pre mean (ms) | pre min (ms) | pre Mframes/s | pre MiB/s " + "| re mean (ms) | re Mframes/s | re MiB/s |" + ) + lines.append("| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |") + for r in results: + pre, re = pick(r) + lines.append( + f"| {r.name} | {r.frames:,} | {_fmt_ms(pre.mean)} | {_fmt_ms(pre.min)} | " + f"{pre.mframes_per_s:.2f} | {pre.mib_per_s:,.0f} | " + f"{_fmt_ms(re.mean)} | {re.mframes_per_s:.2f} | {re.mib_per_s:,.0f} |" + ) + lines.append("") + + _table( + "`parse_bulk` (core zero-copy parse)", + "`pre` = parse a pre-read buffer; `re` = re-read the file from disk each run. " + "Note `parse_bulk` only builds lazy strided views — it does **not** run " + "converters — so timings are near-uniform regardless of payload shape.", + lambda r: (r.bulk_preread, r.bulk_reread), + ) + _table( + "`parse_to_dataframe` (full path to pandas, copy=False)", + "`pre` = parse a pre-read buffer; `re` = read file + parse each run.", + lambda r: (r.df_preread, r.df_reread), + ) + + # Decode-only table (single mode): to_columns() runs every field's + # converter.decode_batch, with no file read and no pandas construction. + lines.append("## `to_columns` (decode only — where converters run)\n") + lines.append( + "Isolates the decode step: `parse_bulk` views are built once up front, then " + "only `payload.to_columns()` is timed. This is where each field's " + "`converter.decode_batch` executes. Registers whose converters loop in Python " + "(`HarpVersionConverter`, `StringConverter`, `BytesToIntConverter` → object " + "dtype) dominate here; vectorized converters stay cheap.\n" + ) + lines.append("| Register | Frames | mean (ms) | min (ms) | stdev (ms) | Mframes/s | MiB/s |") + lines.append("| --- | ---: | ---: | ---: | ---: | ---: | ---: |") + for r in results: + c = r.cols + lines.append( + f"| {r.name} | {r.frames:,} | {_fmt_ms(c.mean)} | {_fmt_ms(c.min)} | " + f"{_fmt_ms(c.stdev)} | {c.mframes_per_s:.2f} | {c.mib_per_s:,.0f} |" + ) + lines.append("") + + # Decomposition: parse_to_dataframe(pre) ≈ parse_bulk(pre) + to_columns + pandas. + lines.append("## Decomposition (pre-read means, ms)\n") + lines.append( + "`parse_to_dataframe` ≈ `parse_bulk` (build views) + `to_columns` (decode) + " + "pandas DataFrame construction. The residual column is `df − bulk − to_columns`, " + "i.e. the pandas/column-assembly overhead. Note the three terms are timed in " + "separate loops, so for converter-dominated registers (large mean, large stdev) " + "the residual is within noise and can even go slightly negative.\n" + ) + lines.append("| Register | parse_bulk | to_columns | parse_to_dataframe | pandas residual |") + lines.append("| --- | ---: | ---: | ---: | ---: |") + for r in results: + residual = r.df_preread.mean - r.bulk_preread.mean - r.cols.mean + lines.append( + f"| {r.name} | {_fmt_ms(r.bulk_preread.mean)} | {_fmt_ms(r.cols.mean)} | " + f"{_fmt_ms(r.df_preread.mean)} | {_fmt_ms(residual)} |" + ) + lines.append("") + + return "\n".join(lines) + + +def _select(only): + selected = BENCHMARK_REGISTERS + if only: + wanted = set(only) + selected = [r for r in BENCHMARK_REGISTERS if r.name in wanted] + missing = wanted - {r.name for r in selected} + if missing: + raise SystemExit(f"Unknown register name(s): {', '.join(sorted(missing))}") + return selected + + +def _prepare_corpora(selected, *, entries: int, force: bool) -> None: + """Generate any missing corpora (cache honored unless ``force``), printing progress.""" + DATA_DIR.mkdir(parents=True, exist_ok=True) + n = len(selected) + mode = "rebuilding" if force else "cache honored" + print(f"Preparing {n} corpus file(s) in {DATA_DIR} ({mode}, {entries:,} frames each):") + for i, reg in enumerate(selected, 1): + print(f" [{i}/{n}] {reg.name:<24s} ", end="", flush=True) + _, generated = ensure_corpus(reg, entries, force=force) + print("generated" if generated else "cached") + print() + + +def _use_utf8_console() -> None: + """Best-effort: make console output UTF-8 (the docstrings/report use ≈, →, −).""" + for stream in (sys.stdout, sys.stderr): + try: + stream.reconfigure(encoding="utf-8") # type: ignore[union-attr] + except (AttributeError, ValueError): + pass + + +def main() -> None: + _use_utf8_console() + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--runs", type=int, default=10, help="repeats per measurement (default: 10)" + ) + parser.add_argument( + "--entries", + type=int, + default=1_000_000, + help="frames per corpus when generating (default: 1,000,000)", + ) + parser.add_argument( + "--force", action="store_true", help="regenerate corpora even if cached files exist" + ) + parser.add_argument( + "--only", nargs="+", metavar="NAME", help="restrict to these register names (default: all)" + ) + parser.add_argument("--report", type=Path, default=REPORT_PATH) + args = parser.parse_args() + + selected = _select(args.only) + + # Ensure the data exists before benchmarking (honor the cache unless --force). + _prepare_corpora(selected, entries=args.entries, force=args.force) + + n = len(selected) + results: list[RegisterResult] = [] + print(f"Benchmarking {n} register(s), {args.runs} runs each:") + for i, reg in enumerate(selected, 1): + path = DATA_DIR / reg.filename + print(f" [{i}/{n}] {reg.name:<24s} ", end="", flush=True) + res = benchmark_register(reg, path, runs=args.runs) + results.append(res) + print( + f"bulk={_fmt_ms(res.bulk_preread.mean):>8s}ms " + f"to_columns={_fmt_ms(res.cols.mean):>9s}ms " + f"df={_fmt_ms(res.df_preread.mean):>9s}ms" + ) + + args.report.parent.mkdir(parents=True, exist_ok=True) + report = build_report(results, runs=args.runs) + args.report.write_text(report, encoding="utf-8") + print(f"\nReport written to {args.report}") + + +if __name__ == "__main__": + main() diff --git a/src/packages/harp-benchmarks/src/harp/benchmarks/generate.py b/src/packages/harp-benchmarks/src/harp/benchmarks/generate.py new file mode 100644 index 0000000..0a1d8e4 --- /dev/null +++ b/src/packages/harp-benchmarks/src/harp/benchmarks/generate.py @@ -0,0 +1,92 @@ +import argparse +import sys + +from harp.benchmarks._registers import BENCHMARK_REGISTERS, DATA_DIR, BenchmarkedRegister + +_TIMESTAMP = 42 + + +def corpus_path(reg: BenchmarkedRegister): + """Path to ``reg``'s corpus file under the artifacts data directory.""" + return DATA_DIR / reg.filename + + +def generate_one(reg: BenchmarkedRegister, entries: int) -> tuple[str, int, int]: + """Write ``entries`` frames for ``reg``. Returns (path, frame_size, file_size).""" + frame = reg.register.format(reg.value, timestamp=_TIMESTAMP) + path = corpus_path(reg) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(frame * entries) + return str(path), len(frame), path.stat().st_size + + +def ensure_corpus( + reg: BenchmarkedRegister, entries: int, *, force: bool = False +) -> tuple[object, bool]: + """Generate ``reg``'s corpus unless a matching cached file already exists. + + The cache is honored only when the existing file's size matches ``entries`` + exactly (frame_size * entries); a stale file (different entry count) is rebuilt. + Returns (path, generated). + """ + path = corpus_path(reg) + if path.exists() and not force: + frame_size = len(reg.register.format(reg.value, timestamp=_TIMESTAMP)) + if path.stat().st_size == frame_size * entries: + return path, False + generate_one(reg, entries) + return path, True + + +def _use_utf8_console() -> None: + """Best-effort: make console output UTF-8 (docstrings use non-ASCII glyphs).""" + for stream in (sys.stdout, sys.stderr): + try: + stream.reconfigure(encoding="utf-8") # type: ignore[union-attr] + except (AttributeError, ValueError): + pass + + +def _select(only): + selected = BENCHMARK_REGISTERS + if only: + wanted = set(only) + selected = [r for r in BENCHMARK_REGISTERS if r.name in wanted] + missing = wanted - {r.name for r in selected} + if missing: + raise SystemExit(f"Unknown register name(s): {', '.join(sorted(missing))}") + return selected + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--entries", type=int, default=1_000_000, help="frames per register (default: 1,000,000)" + ) + parser.add_argument( + "--only", + nargs="+", + metavar="NAME", + help="restrict generation to these register names (default: all)", + ) + _use_utf8_console() + args = parser.parse_args() + + selected = _select(args.only) + DATA_DIR.mkdir(parents=True, exist_ok=True) + + print(f"Generating {args.entries:,} frames/register into {DATA_DIR}\n") + total_bytes = 0 + for reg in selected: + _, frame_size, file_size = generate_one(reg, args.entries) + total_bytes += file_size + print( + f" {reg.name:<24s} addr={reg.address:<4d} " + f"frame={frame_size:>3d}B -> {reg.filename:<32s} " + f"({file_size / 1024**2:8.1f} MiB)" + ) + print(f"\nDone. {len(selected)} file(s), {total_bytes / 1024**2:,.1f} MiB total.") + + +if __name__ == "__main__": + main() diff --git a/tests/protocol/register_models.py b/src/packages/harp-benchmarks/src/harp/benchmarks/register_models.py similarity index 91% rename from tests/protocol/register_models.py rename to src/packages/harp-benchmarks/src/harp/benchmarks/register_models.py index a7e7e2a..43802f7 100644 --- a/tests/protocol/register_models.py +++ b/src/packages/harp-benchmarks/src/harp/benchmarks/register_models.py @@ -1,27 +1,3 @@ -"""Reference models for every register in the canonical generator test device -(``harp-tech/generators`` -> ``tests/Metadata/device.yml``), built with the -``harp.protocol`` public API. - -These act as fixtures for ``test_register_modeling.py`` and demonstrate that the -API can express every payload shape the Harp protocol allows (gaps, overlaps, -masks, reinterpreted sub-regions, custom domain types) before building a -spec->code generator. Run it directly to round-trip every register:: - - uv run python -m tests.protocol.register_models - -Design (see notes/payload_api_redesign.md): - -* A payload is a flat buffer of base-``type`` elements; each member is a typed - view ``(offset, mask, converter)``. ``offset`` is in base-element units. -* ``Converter`` instances operate on their own byte layout and are independent of - the register element type (custom codecs read raw ``uint8`` sub-arrays), so the - same ``HarpVersionConverter`` works under a U8 or a U32 register. -* Masked sub-fields use ``GroupMask`` (enum) or ``Field(converter=..., mask=...)`` - (numeric); the right-shift is derived from the mask's trailing zeros. -* The register ``length`` (base elements) fixes ``itemsize`` so byte gaps survive. -* Enum decoding is strict: an out-of-range code raises. -""" - import enum from typing import Any, ClassVar @@ -48,6 +24,10 @@ StructPayload, ) +# This file targets the device.yml here +# https://raw.githubusercontent.com/harp-tech/generators/refs/heads/main/tests/Metadata/device.yml + + # =========================================================================== # device.yml bitMasks + groupMasks # =========================================================================== @@ -387,9 +367,9 @@ def main() -> None: # pragma: no cover - manual exploration entry point np.testing.assert_array_equal(p.InterfaceHash, np.arange(20)) print(f"Version OK ({VersionPayload.dtype.itemsize} bytes)") - p = _roundtrip(CustomPayload, CustomPayloadPayload(value=HarpVersion(3, 1, 4))) + p = _roundtrip(CustomPayload, HarpVersion(3, 1, 4)) assert p == HarpVersion(3, 1, 4) # single-member unwrap -> bare HarpVersion - p = _roundtrip(CustomRawPayload, CustomRawPayloadPayload(value=HarpVersion(0, 0, 1))) + p = _roundtrip(CustomRawPayload, HarpVersion(0, 0, 1)) assert p == HarpVersion(0, 0, 1) print("CustomPayload/RawPayload OK (single-member unwrap)") diff --git a/tests/protocol/test_register_modeling.py b/tests/protocol/test_register_modeling.py index 186c7c7..50ec3ad 100644 --- a/tests/protocol/test_register_modeling.py +++ b/tests/protocol/test_register_modeling.py @@ -1,5 +1,5 @@ """Acceptance tests: the device.yml coverage model in -``tests.protocol.register_models`` round-trips, and the new API behaviours +``harp.benchmarks.register_models`` round-trips, and the new API behaviours (offsets/gaps, masked overlap, single-member unwrap, strict enums) hold. """ @@ -7,7 +7,7 @@ import pytest from harp.data import payload_to_dataframe from harp.protocol import HarpMessage, HarpVersion -from tests.protocol.register_models import ( +from harp.benchmarks.register_models import ( AnalogData, AnalogDataPayload, BitmaskSplitter, diff --git a/uv.lock b/uv.lock index 2cb1996..4254a70 100644 --- a/uv.lock +++ b/uv.lock @@ -13,6 +13,7 @@ resolution-markers = [ [manifest] members = [ "harp", + "harp-benchmarks", "harp-data", "harp-device", "harp-protocol", @@ -357,6 +358,7 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "harp-benchmarks" }, { name = "harp-data" }, { name = "harp-device" }, { name = "harp-protocol" }, @@ -389,6 +391,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ + { name = "harp-benchmarks", editable = "src/packages/harp-benchmarks" }, { name = "harp-data", editable = "src/packages/harp-data" }, { name = "harp-device", editable = "src/packages/harp-device" }, { name = "harp-protocol", editable = "src/packages/harp-protocol" }, @@ -411,6 +414,25 @@ docs = [ { name = "mkdocstrings-python", specifier = ">=1.16.6" }, ] +[[package]] +name = "harp-benchmarks" +version = "0.1.0" +source = { editable = "src/packages/harp-benchmarks" } +dependencies = [ + { name = "harp-data" }, + { name = "harp-protocol" }, + { name = "numpy" }, + { name = "pandas" }, +] + +[package.metadata] +requires-dist = [ + { name = "harp-data", editable = "src/packages/harp-data" }, + { name = "harp-protocol", editable = "src/packages/harp-protocol" }, + { name = "numpy", specifier = ">=1.24" }, + { name = "pandas", specifier = ">=2.0" }, +] + [[package]] name = "harp-data" version = "0.1.0" From 6922908f8a74db715aade253d31bd73cb69eb7b5 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:46:07 -0700 Subject: [PATCH 249/267] Add converter for faster parsing --- .../src/harp/benchmarks/register_models.py | 12 +++++------- .../src/harp/protocol/_payload_converters.py | 12 ++++++++---- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/packages/harp-benchmarks/src/harp/benchmarks/register_models.py b/src/packages/harp-benchmarks/src/harp/benchmarks/register_models.py index 43802f7..d6aac12 100644 --- a/src/packages/harp-benchmarks/src/harp/benchmarks/register_models.py +++ b/src/packages/harp-benchmarks/src/harp/benchmarks/register_models.py @@ -73,19 +73,17 @@ class BytesToIntConverter(Converter[int]): def __init__(self, length: int, *, signed: bool = False) -> None: self._length = length self._signed = signed + endian = "<" if length > 1 else "" + kind = "i" if signed else "u" + self._native = IdentityConverter(f"{endian}{kind}{length}") self.dtype = np.dtype((np.uint8, (length,))) def decode_scalar(self, view: np.generic) -> int: return int.from_bytes(bytes(np.asarray(view).tolist()), "little", signed=self._signed) def decode_batch(self, view: NDArray[np.generic]) -> Any: - return np.array( - [ - int.from_bytes(bytes(np.asarray(r).tolist()), "little", signed=self._signed) - for r in np.atleast_2d(view) - ], - dtype=object, - ) + rows = np.atleast_2d(view).reshape(-1, self._length) + return self._native.decode_batch(rows.view(self._native.dtype).reshape(-1)) def encode_into(self, view: NDArray[np.generic], value: int) -> None: view[...] = np.frombuffer( diff --git a/src/packages/harp-protocol/src/harp/protocol/_payload_converters.py b/src/packages/harp-protocol/src/harp/protocol/_payload_converters.py index 560df91..859727f 100644 --- a/src/packages/harp-protocol/src/harp/protocol/_payload_converters.py +++ b/src/packages/harp-protocol/src/harp/protocol/_payload_converters.py @@ -173,11 +173,15 @@ def __init__(self, length: int, encoding: str = "ascii") -> None: def decode_scalar(self, view: np.generic) -> str: return bytes(view).rstrip(b"\x00").decode(self._encoding) # ty: ignore[invalid-argument-type] + _VIEW_CASTABLE_ENCODINGS = frozenset({"ascii", "latin1", "latin-1", "iso-8859-1"}) + def decode_batch(self, view: NDArray[np.generic]) -> Any: - return np.array( - [bytes(row).rstrip(b"\x00").decode(self._encoding) for row in view], - dtype=object, - ) + if self._encoding not in self._VIEW_CASTABLE_ENCODINGS: + return np.array( + [bytes(row).rstrip(b"\x00").decode(self._encoding) for row in view], + dtype=object, + ) + return view.reshape(-1, self._length).view(f"S{self._length}").reshape(-1).astype(str) def encode_into(self, view: NDArray[np.generic], value: str) -> None: encoded = value.encode(self._encoding)[: self._length] From 1de2f4366782c4221576923a0c93fcc4912ff43c Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Wed, 1 Jul 2026 16:12:08 -0700 Subject: [PATCH 250/267] Optionally print dataframes --- .../harp-benchmarks/src/harp/benchmarks/benchmark.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/packages/harp-benchmarks/src/harp/benchmarks/benchmark.py b/src/packages/harp-benchmarks/src/harp/benchmarks/benchmark.py index 19f3b36..191c939 100644 --- a/src/packages/harp-benchmarks/src/harp/benchmarks/benchmark.py +++ b/src/packages/harp-benchmarks/src/harp/benchmarks/benchmark.py @@ -302,6 +302,11 @@ def main() -> None: "--only", nargs="+", metavar="NAME", help="restrict to these register names (default: all)" ) parser.add_argument("--report", type=Path, default=REPORT_PATH) + parser.add_argument( + "--head", + action="store_true", + help="print head(5) of each register's DataFrame to stdout (not saved to the report)", + ) args = parser.parse_args() selected = _select(args.only) @@ -322,6 +327,10 @@ def main() -> None: f"to_columns={_fmt_ms(res.cols.mean):>9s}ms " f"df={_fmt_ms(res.df_preread.mean):>9s}ms" ) + if args.head: + df = parse_to_dataframe(reg.register, path.read_bytes(), timestamp=True) + print(df.head(5)) + print() args.report.parent.mkdir(parents=True, exist_ok=True) report = build_report(results, runs=args.runs) From 70d3db882352fb8acd5a0d8c20539d04ea7c74ad Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Thu, 2 Jul 2026 08:37:05 -0700 Subject: [PATCH 251/267] Add numpy broadcasting to HarpVersion conversion --- .../harp-protocol/src/harp/protocol/_payload_converters.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/packages/harp-protocol/src/harp/protocol/_payload_converters.py b/src/packages/harp-protocol/src/harp/protocol/_payload_converters.py index 859727f..545e2c8 100644 --- a/src/packages/harp-protocol/src/harp/protocol/_payload_converters.py +++ b/src/packages/harp-protocol/src/harp/protocol/_payload_converters.py @@ -212,10 +212,8 @@ def decode_scalar(self, view: np.generic) -> HarpVersion: return HarpVersion(int(c[0]), int(c[1]), int(c[2])) def decode_batch(self, view: NDArray[np.generic]) -> Any: - return np.array( - [HarpVersion(int(r[0]), int(r[1]), int(r[2])) for r in np.atleast_2d(view)], - dtype=object, - ) + v = np.atleast_2d(view) + return np.frompyfunc(HarpVersion, 3, 1)(v[:, 0], v[:, 1], v[:, 2]) def encode_into(self, view: NDArray[np.generic], value: HarpVersion) -> None: view[...] = np.array([value.major, value.minor, value.patch], dtype=self.dtype.base) From d7ed4b7b3f8a8002b2641ab63d91e776b95ce438 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Thu, 2 Jul 2026 09:58:35 -0700 Subject: [PATCH 252/267] Lazily parse the timestamp --- .../src/harp/benchmarks/_registers.py | 18 +++++++- .../src/harp/benchmarks/benchmark.py | 4 +- .../src/harp/benchmarks/generate.py | 8 +++- .../src/harp/protocol/_register.py | 46 ++++++++++++++++++- 4 files changed, 69 insertions(+), 7 deletions(-) diff --git a/src/packages/harp-benchmarks/src/harp/benchmarks/_registers.py b/src/packages/harp-benchmarks/src/harp/benchmarks/_registers.py index 808bb83..7033400 100644 --- a/src/packages/harp-benchmarks/src/harp/benchmarks/_registers.py +++ b/src/packages/harp-benchmarks/src/harp/benchmarks/_registers.py @@ -58,6 +58,7 @@ class BenchmarkedRegister(NamedTuple): name: str register: type[RegisterBase[Any]] value: Any + timestamped: bool = True @property def address(self) -> int: @@ -68,7 +69,8 @@ def filename(self) -> str: return f"{self.name}_{self.address}.bin" -def _build() -> list[BenchmarkedRegister]: +def _base_registers() -> list[BenchmarkedRegister]: + """One (timestamped) fixture per register — :func:`_build` derives the untimestamped twin.""" return [ BenchmarkedRegister("DigitalInputs", DigitalInputs, np.uint8(0b1010)), BenchmarkedRegister( @@ -142,4 +144,18 @@ def _build() -> list[BenchmarkedRegister]: ] +def _build() -> list[BenchmarkedRegister]: + """Expand every base fixture into a timestamped + untimestamped pair. + + ``parse_bulk`` detects timestamping from the wire format at runtime (not + statically), so every register needs a corpus of each shape to exercise both + the eager (``timestamps`` present) and ``None`` decode paths. + """ + registers: list[BenchmarkedRegister] = [] + for reg in _base_registers(): + registers.append(reg) + registers.append(reg._replace(name=f"{reg.name}NoTimestamp", timestamped=False)) + return registers + + BENCHMARK_REGISTERS: list[BenchmarkedRegister] = _build() diff --git a/src/packages/harp-benchmarks/src/harp/benchmarks/benchmark.py b/src/packages/harp-benchmarks/src/harp/benchmarks/benchmark.py index 191c939..19a36dc 100644 --- a/src/packages/harp-benchmarks/src/harp/benchmarks/benchmark.py +++ b/src/packages/harp-benchmarks/src/harp/benchmarks/benchmark.py @@ -111,13 +111,13 @@ def benchmark_register(reg: BenchmarkedRegister, path: Path, *, runs: int) -> Re file_bytes=file_bytes, ) df_pre = _time( - lambda: parse_to_dataframe(register, raw, timestamp=True), + lambda: parse_to_dataframe(register, raw, timestamp=reg.timestamped), runs=runs, frames=frames, file_bytes=file_bytes, ) df_re = _time( - lambda: parse_to_dataframe(register, path.read_bytes(), timestamp=True), + lambda: parse_to_dataframe(register, path.read_bytes(), timestamp=reg.timestamped), runs=runs, frames=frames, file_bytes=file_bytes, diff --git a/src/packages/harp-benchmarks/src/harp/benchmarks/generate.py b/src/packages/harp-benchmarks/src/harp/benchmarks/generate.py index 0a1d8e4..17787de 100644 --- a/src/packages/harp-benchmarks/src/harp/benchmarks/generate.py +++ b/src/packages/harp-benchmarks/src/harp/benchmarks/generate.py @@ -11,9 +11,13 @@ def corpus_path(reg: BenchmarkedRegister): return DATA_DIR / reg.filename +def _frame_timestamp(reg: BenchmarkedRegister) -> int | None: + return _TIMESTAMP if reg.timestamped else None + + def generate_one(reg: BenchmarkedRegister, entries: int) -> tuple[str, int, int]: """Write ``entries`` frames for ``reg``. Returns (path, frame_size, file_size).""" - frame = reg.register.format(reg.value, timestamp=_TIMESTAMP) + frame = reg.register.format(reg.value, timestamp=_frame_timestamp(reg)) path = corpus_path(reg) path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(frame * entries) @@ -31,7 +35,7 @@ def ensure_corpus( """ path = corpus_path(reg) if path.exists() and not force: - frame_size = len(reg.register.format(reg.value, timestamp=_TIMESTAMP)) + frame_size = len(reg.register.format(reg.value, timestamp=_frame_timestamp(reg))) if path.stat().st_size == frame_size * entries: return path, False generate_one(reg, entries) diff --git a/src/packages/harp-protocol/src/harp/protocol/_register.py b/src/packages/harp-protocol/src/harp/protocol/_register.py index 6311f51..9f221dc 100644 --- a/src/packages/harp-protocol/src/harp/protocol/_register.py +++ b/src/packages/harp-protocol/src/harp/protocol/_register.py @@ -47,6 +47,48 @@ _AR = TypeVar("_AR", bound="RegisterBase[Any]") +class _LazyTimestamps: + """Seconds + microseconds timestamp views, combined into float64 on first use. + + Combining the raw views costs an O(n) pass over every frame (two ``astype`` + casts plus a multiply-add), independent of the register's payload — so eagerly + computing it in ``parse_bulk`` taxes every call even when the caller never + reads the timestamps. Deferring the combine until the array is actually + accessed (and caching the result) avoids that cost in the common case where + only the payload is needed. + """ + + __slots__ = ("_ts_s", "_ts_us", "_values") + + def __init__(self, ts_s: np.ndarray, ts_us: np.ndarray) -> None: + self._ts_s = ts_s + self._ts_us = ts_us + self._values: np.ndarray | None = None + + def _resolve(self) -> np.ndarray: + if self._values is None: + out = np.multiply(self._ts_us, _TICK_PERIOD_S, dtype=np.float64) + np.add(self._ts_s, out, out=out) + self._values = out + return self._values + + def __array__(self, dtype: "np.dtype | None" = None) -> np.ndarray: + arr = self._resolve() + return arr if dtype is None else arr.astype(dtype) + + def __len__(self) -> int: + return len(self._ts_s) + + def __iter__(self): + return iter(self._resolve()) + + def __getitem__(self, item: Any) -> Any: + return self._resolve()[item] + + def __repr__(self) -> str: + return repr(self._resolve()) + + class _RegisterMeta(ABCMeta): """Calling a register class with an address creates a one-off subclass: ``RegisterU32(0x08)``.""" @@ -99,7 +141,7 @@ def parse_bulk( source: bytes | bytearray | memoryview, *, parse_timestamp: bool = True, - ) -> "tuple[np.ndarray, np.ndarray | None, np.ndarray | None, Batch[Any]]": + ) -> "tuple[np.ndarray, _LazyTimestamps | None, np.ndarray | None, Batch[Any]]": """Parse a bulk buffer containing one or more frames of this register type. Returns (data, timestamps, msgtype_view, payload).""" # Returns (data, timestamps, msgtype_view, payload). ``data`` is payload_cls = cls.payload_class @@ -122,7 +164,7 @@ def parse_bulk( ts_us = np.ndarray( nrows, dtype=" Date: Fri, 17 Jul 2026 08:29:22 -0700 Subject: [PATCH 253/267] Fix stale references Co-authored-by: glopesdev --- LICENSE | 1 + README.md | 4 ++-- docs/api/protocol.md | 14 ++++++++------ docs/api/serial.md | 4 ++-- .../read_data_to_dataframe.md | 2 +- .../read_data_to_dataframe.py | 2 +- mkdocs.yml | 10 +++++----- pyproject.toml | 8 ++++---- src/packages/harp-protocol/pyproject.toml | 2 +- 9 files changed, 25 insertions(+), 22 deletions(-) diff --git a/LICENSE b/LICENSE index b7afd7f..2ac2366 100644 --- a/LICENSE +++ b/LICENSE @@ -2,6 +2,7 @@ MIT License Copyright (c) 2020 OEPS & Filipe Carvalho Copyright (c) 2025 Hardware and Software Platform, Champalimaud Foundation +Copyright (c) harp-tech and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 811f3d9..5f2d9fc 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,10 @@ This project includes two main packages: - **harp-protocol**: Provides the core protocol definitions and utilities for the Harp protocol. - See [Protocol API Documentation](https://fchampalimaud.github.io/pyharp/api/protocol) for details. + See [Protocol API Documentation](https://harp-tech.org/pyharp/api/protocol) for details. - **harp-serial**: Implements serial communication functionalities for generic Harp devices. - See [Serial API Documentation](https://fchampalimaud.github.io/pyharp/api/serial) for more information. + See [Serial API Documentation](https://harp-tech.org/pyharp/api/serial) for more information. For specific Harp devices' packages please select the corresponding Harp device under the Devices section on the menu. \ No newline at end of file diff --git a/docs/api/protocol.md b/docs/api/protocol.md index 2f065fd..0a43ce2 100644 --- a/docs/api/protocol.md +++ b/docs/api/protocol.md @@ -4,9 +4,11 @@ ::: harp.protocol.MessageType ::: harp.protocol.PayloadType -::: harp.protocol.CommonRegisters -::: harp.protocol.OperationMode -::: harp.protocol.OperationCtrl -::: harp.protocol.ResetMode -::: harp.protocol.ClockConfig -::: harp.protocol.messages.HarpMessage +::: harp.protocol.HarpMessage +::: harp.protocol.RegisterBase +::: harp.protocol.StructPayload +::: harp.protocol.AnonymousPayload +::: harp.protocol.Field +::: harp.protocol.GroupMask +::: harp.protocol.BitMask +::: harp.protocol.Converter diff --git a/docs/api/serial.md b/docs/api/serial.md index e8d7bd4..44bfe72 100644 --- a/docs/api/serial.md +++ b/docs/api/serial.md @@ -2,5 +2,5 @@ --- -::: harp.serial.Device -::: harp.serial.TimeoutStrategy +::: harp.serial.SerialTransport +::: harp.serial.open_serial_device diff --git a/docs/examples/read_data_to_dataframe/read_data_to_dataframe.md b/docs/examples/read_data_to_dataframe/read_data_to_dataframe.md index 9162774..a205b79 100644 --- a/docs/examples/read_data_to_dataframe/read_data_to_dataframe.md +++ b/docs/examples/read_data_to_dataframe/read_data_to_dataframe.md @@ -1,7 +1,7 @@ # Reading Data into a DataFrame This example demonstrates how to load a Harp register's binary data file into a -pandas DataFrame using `harp.data`. The register definition tells `read_dataframe` +pandas DataFrame using `harp.data`. The register definition tells `parse_to_dataframe` how to decode each frame, so you get named columns (and decoded enums) for free. diff --git a/docs/examples/read_data_to_dataframe/read_data_to_dataframe.py b/docs/examples/read_data_to_dataframe/read_data_to_dataframe.py index 6f1459c..fda5a2d 100644 --- a/docs/examples/read_data_to_dataframe/read_data_to_dataframe.py +++ b/docs/examples/read_data_to_dataframe/read_data_to_dataframe.py @@ -6,6 +6,6 @@ df = parse_to_dataframe(OperationControl, "OperationControl.bin", timestamp=True) print(df.head()) -# `read_dataframe` also accepts raw bytes or an open binary file object: +# `parse_to_dataframe` also accepts raw bytes or an open binary file object: with open("OperationControl.bin", "rb") as f: df = parse_to_dataframe(OperationControl, f) diff --git a/mkdocs.yml b/mkdocs.yml index 7143242..75290ef 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,7 +1,7 @@ site_name: pyharp -repo_url: https://github.com/fchampalimaud/pyharp -repo_name: "fchampalimaud/pyharp" -copyright: Copyright © 2025, Hardware and Software Platform, Champalimaud Foundation +repo_url: https://github.com/harp-tech/pyharp +repo_name: "harp-tech/pyharp" +copyright: Copyright (c) harp-tech and Contributors plugins: - search @@ -20,7 +20,7 @@ plugins: extensions: - griffe_fieldz - git-committers: - repository: fchampalimaud/pyharp + repository: harp-tech/pyharp branch: main - git-authors @@ -89,7 +89,7 @@ nav: extra: social: - icon: fontawesome/brands/github - link: https://github.com/fchampalimaud/pyharp + link: https://github.com/harp-tech/pyharp extra_css: - stylesheets/extra.css diff --git a/pyproject.toml b/pyproject.toml index 550a1e2..0688c27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "harp" version = "0.2.0" description = "Library for data acquisition and control of devices implementing the Harp protocol." -authors = [{ name= "Hardware and Software Platform, Champalimaud Foundation", email="software@research.fchampalimaud.org"}] +authors = [{ name = "harp-tech", email = "contact@harp-tech.org" }] license = "MIT" readme = 'README.md' keywords = ['python', 'harp'] @@ -15,9 +15,9 @@ dependencies = [ ] [project.urls] -Repository = "https://github.com/fchampalimaud/pyharp/" -"Bug Tracker" = "https://github.com/fchampalimaud/pyharp/issues" -Documentation = "https://fchampalimaud.github.io/pyharp/" +Repository = "https://github.com/harp-tech/pyharp/" +"Bug Tracker" = "https://github.com/harp-tech/pyharp/issues" +Documentation = "https://harp-tech.org/pyharp/" [tool.uv.sources] harp-protocol = { workspace = true } diff --git a/src/packages/harp-protocol/pyproject.toml b/src/packages/harp-protocol/pyproject.toml index fd44395..5147dde 100644 --- a/src/packages/harp-protocol/pyproject.toml +++ b/src/packages/harp-protocol/pyproject.toml @@ -2,7 +2,7 @@ name = "harp-protocol" version = "0.4.0" description = "Library with the base types for Harp protocol usage." -authors = [{ name= "Hardware and Software Platform, Champalimaud Foundation", email="software@research.fchampalimaud.org"}] +authors = [{ name = "harp-tech", email = "contact@harp-tech.org" }] license = "MIT" keywords = ['python', 'harp'] requires-python = ">=3.10,<4.0" From 68274c3a222b747826e3e12aa409951276dccda4 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 17 Jul 2026 08:48:29 -0700 Subject: [PATCH 254/267] Refactor CICD and remove stale testing files --- .github/workflows/build-docs.yml | 55 ----------- .github/workflows/pyharp.yml | 158 ++++++++++++++++++++++++++++++ README.md | 17 +++- docs/api/data.md | 6 ++ docs/api/device.md | 15 +++ docs/assets/logo.svg | 71 -------------- mkdocs.yml | 6 +- pyproject.toml | 1 - run_docs.ps1 | 31 ------ run_docs.sh | 19 ---- scripts/core_registers_example.py | 116 ---------------------- scripts/device_integration.py | 101 ------------------- scripts/heterogenous_register.py | 92 ----------------- uv.lock | 36 ------- 14 files changed, 196 insertions(+), 528 deletions(-) delete mode 100644 .github/workflows/build-docs.yml create mode 100644 .github/workflows/pyharp.yml create mode 100644 docs/api/data.md create mode 100644 docs/api/device.md delete mode 100644 docs/assets/logo.svg delete mode 100644 run_docs.ps1 delete mode 100755 run_docs.sh delete mode 100644 scripts/core_registers_example.py delete mode 100644 scripts/device_integration.py delete mode 100644 scripts/heterogenous_register.py diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml deleted file mode 100644 index 0ad6bea..0000000 --- a/.github/workflows/build-docs.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: Build Documentation - -on: - workflow_dispatch: - -jobs: - build-docs: - runs-on: ubuntu-latest - steps: - - name: Checkout pyharp repository - uses: actions/checkout@v4 - - - name: Clone harp.devices repository - uses: actions/checkout@v4 - with: - repository: fchampalimaud/harp.devices - token: ${{ secrets.HARP_DEVICES_TOKEN }} - path: harp.devices - - - name: Install uv - uses: astral-sh/setup-uv@v5 - - - name: Install project dependencies - run: | - uv sync --group docs --group dev - - - name: Build documentation - run: | - chmod +x ./run_docs.sh - ./run_docs.sh build - - - name: Upload Build Artifact - uses: actions/upload-pages-artifact@v3 - with: - path: site - - deploy: - name: Deploy to Github pages - needs: build-docs - - # Grant GITHUB_TOKEN the permissions required to make a Pages deployment - permissions: - pages: write # to deploy to Pages - id-token: write # to verify the deployment originates from an appropriate source - - # Deploy to the github-pages environment - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - - runs-on: ubuntu-latest - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/.github/workflows/pyharp.yml b/.github/workflows/pyharp.yml new file mode 100644 index 0000000..18a01cc --- /dev/null +++ b/.github/workflows/pyharp.yml @@ -0,0 +1,158 @@ +name: pyharp test suite + +on: + pull_request: + push: + branches: + - master + release: + types: [published] + workflow_dispatch: + +jobs: + # ---- Tests ---- + tests: + name: Python ${{ matrix.python-version }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.10", "3.11", "3.12", "3.13"] + fail-fast: false + steps: + - uses: actions/checkout@v7 + + - uses: astral-sh/setup-uv@v8.3.2 + with: + enable-cache: true + + - name: Install python dependencies + run: uv sync --group dev + + - name: Run ruff format + run: uv run ruff format --check + + - name: Run ruff check + run: uv run ruff check + + - name: Run ty + run: uv run ty check + + - name: Run pytest + run: uv run pytest --cov harp + + - name: Build + run: uv build --all-packages + + prepare-release: + runs-on: ubuntu-latest + name: Set version from release tag + needs: tests + if: github.event_name == 'release' && github.event.action == 'published' + outputs: + version: ${{ steps.get_version.outputs.version }} + + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + ref: master + + - uses: astral-sh/setup-uv@v8.3.2 + with: + enable-cache: true + + - name: Extract version from tag + id: get_version + shell: bash + run: | + version=$(echo "${{ github.event.release.tag_name }}" | sed 's/^v//') + echo "version=$version" >> $GITHUB_OUTPUT + echo "Setting version to: $version" + + - name: Validate version format + run: uv version ${{ steps.get_version.outputs.version }} --dry-run + + - name: Set version + run: uv version ${{ steps.get_version.outputs.version }} + + - name: Build + run: uv build --all-packages + + - name: Remove internal-only packages from dist + shell: bash + run: rm -f dist/harp_benchmarks-* + + - name: Upload wheels as artifact + uses: actions/upload-artifact@v7 + with: + name: dist + path: dist/ + + # This step seems a bit complicated, but it allows for idempotent commits, + # so that if the release tag is moved to a new commit, + # the version will be updated and committed again. + - name: Commit version changes + shell: bash + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git add . + git diff --cached --quiet && echo "No changes to commit" && exit 0 + git commit -m "Set version ${{ steps.get_version.outputs.version }} [skip ci]" + git push origin master + # Move the release tag to point to this new commit. + git tag -fa "${{ github.event.release.tag_name }}" -m "${{ github.event.release.tag_name }}" + git push origin "${{ github.event.release.tag_name }}" --force + + # ---- Publish to PyPI ---- + publish-to-pypi: + runs-on: ubuntu-latest + name: Publish to PyPI + needs: prepare-release + steps: + - name: Download wheels artifact + uses: actions/download-artifact@v8 + with: + name: dist + path: dist/ + + - uses: astral-sh/setup-uv@v8.3.2 + with: + enable-cache: true + + - name: Publish to PyPI + run: uv publish --token ${{ secrets.PYPI_TOKEN }} + + - name: Upload wheels to GitHub release + uses: softprops/action-gh-release@v3 + with: + files: dist/* + + # ---- Docs ---- + build-docs: + name: Build and deploy documentation to GitHub Pages + runs-on: ubuntu-latest + needs: prepare-release + if: github.event_name == 'release' && !github.event.release.prerelease + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + ref: master + + - name: Install uv + uses: astral-sh/setup-uv@v8.3.2 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --group docs + + - name: Configure Git user + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Build & Deploy docs + run: uv run mkdocs gh-deploy --force diff --git a/README.md b/README.md index 5f2d9fc..02b0a30 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # pyharp -This project includes two main packages: +This project includes four main packages: - **harp-protocol**: Provides the core protocol definitions and utilities for the Harp protocol. See [Protocol API Documentation](https://harp-tech.org/pyharp/api/protocol) for details. @@ -8,5 +8,18 @@ This project includes two main packages: - **harp-serial**: Implements serial communication functionalities for generic Harp devices. See [Serial API Documentation](https://harp-tech.org/pyharp/api/serial) for more information. + - **harp-device**: Implements the transport-agnostic `Device` interface, the common register map, and the shared registers and enums. + See [Device API Documentation](https://harp-tech.org/pyharp/api/device) for details. -For specific Harp devices' packages please select the corresponding Harp device under the Devices section on the menu. \ No newline at end of file + - **harp-data**: Parses register binary dumps into pandas DataFrames. + See [Data API Documentation](https://harp-tech.org/pyharp/api/data) for more information. + +## Building the documentation + +Install the docs dependency group and run mkdocs through uv: + +```sh +uv sync --group docs --group dev +uv run mkdocs serve # live-reloading local preview +uv run mkdocs build # static site in ./site +``` diff --git a/docs/api/data.md b/docs/api/data.md new file mode 100644 index 0000000..b79926a --- /dev/null +++ b/docs/api/data.md @@ -0,0 +1,6 @@ +{% include-markdown "../../src/packages/harp-data/README.md" %} + +--- + +::: harp.data.parse_to_dataframe +::: harp.data.payload_to_dataframe diff --git a/docs/api/device.md b/docs/api/device.md new file mode 100644 index 0000000..dfcade9 --- /dev/null +++ b/docs/api/device.md @@ -0,0 +1,15 @@ +{% include-markdown "../../src/packages/harp-device/README.md" %} + +--- + +::: harp.device.Device +::: harp.device.HarpFramer +::: harp.device.ITransport +::: harp.device.TransportError +::: harp.device.REGISTER_MAP +::: harp.device.OperationControl +::: harp.device.OperationMode +::: harp.device.ResetDevice +::: harp.device.ResetFlags +::: harp.device.ClockConfiguration +::: harp.device.ClockConfigurationFlags diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg deleted file mode 100644 index 96224d9..0000000 --- a/docs/assets/logo.svg +++ /dev/null @@ -1,71 +0,0 @@ - - - -image/svg+xml - - - - - - - - - - diff --git a/mkdocs.yml b/mkdocs.yml index 75290ef..9d6b086 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -7,7 +7,6 @@ plugins: - search - autorefs - codeinclude - - monorepo - include-markdown - mkdocstrings: handlers: @@ -44,7 +43,6 @@ theme: name: material icon: repo: fontawesome/brands/github - logo: assets/logo.svg favicon: assets/favicon.png features: - content.tooltips @@ -80,11 +78,11 @@ nav: - Getting Device Info: examples/get_info/get_info.md - Read and Write from Registers: examples/read_and_write_from_registers/read_and_write_from_registers.md - Reading Data into a DataFrame: examples/read_data_to_dataframe/read_data_to_dataframe.md - - Devices: '*include ./harp.devices/*/docs/examples.mkdocs.yml' - API: - - Devices: '*include ./harp.devices/*/mkdocs.yml' - Protocol: api/protocol.md - Serial: api/serial.md + - Device: api/device.md + - Data: api/data.md extra: social: diff --git a/pyproject.toml b/pyproject.toml index 0688c27..c522e9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,6 @@ docs = [ "mkdocs-git-committers-plugin-2>=2.5.0", "mkdocs-include-markdown-plugin>=7.1.6", "mkdocs-material>=9.6.9", - "mkdocs-monorepo-plugin>=1.1.2", "mkdocstrings-python>=1.16.6", ] diff --git a/run_docs.ps1 b/run_docs.ps1 deleted file mode 100644 index 913404a..0000000 --- a/run_docs.ps1 +++ /dev/null @@ -1,31 +0,0 @@ -# Resolve script directory (optional) and change to that directory if desired: -# Set-Location -Path (Split-Path -Path $MyInvocation.MyCommand.Definition -Parent) - -# Launch mkdocs via uv (argument handling) -param( - [string]$action = "" -) - -# Find all subdirectories of .\harp.devices (follow symlinks) -$harpRoot = Join-Path -Path (Get-Location) -ChildPath "harp.devices" -$dirs = Get-ChildItem -LiteralPath $harpRoot -Directory -Force -ErrorAction SilentlyContinue | - ForEach-Object { $_.FullName } - -# Join them with ':' (Unix-style path separator) -$harpDevicesPaths = ($dirs -join ";") - -# Prepend to PYTHONPATH (preserve existing) -if ($env:PYTHONPATH) { - $env:PYTHONPATH = "{0};{1}" -f $harpDevicesPaths, $env:PYTHONPATH -} else { - $env:PYTHONPATH = $harpDevicesPaths -} - -# Optionally print for debugging -Write-Output "PYTHONPATH set to: $env:PYTHONPATH" - -switch ($action) { - "build" { uv run mkdocs build } - "deploy" { uv run mkdocs gh-deploy } - default { uv run mkdocs serve } -} diff --git a/run_docs.sh b/run_docs.sh deleted file mode 100755 index d95ab3c..0000000 --- a/run_docs.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -# Find all subdirectories of harp.devices and join them with ':' -HARP_DEVICES_PATHS=$(find -L ./harp.devices -mindepth 1 -maxdepth 1 -type d | paste -sd ":" -) - -# Prepend to PYTHONPATH (preserve existing PYTHONPATH) -export PYTHONPATH="$HARP_DEVICES_PATHS:$PYTHONPATH" - -# Optionally print for debugging -echo "PYTHONPATH set to: $PYTHONPATH" - -# Launch mkdocs build or serve -if [ "$1" = "build" ]; then - uv run mkdocs build -elif [ "$1" = "deploy" ]; then - uv run mkdocs gh-deploy -else - uv run mkdocs serve -fi diff --git a/scripts/core_registers_example.py b/scripts/core_registers_example.py deleted file mode 100644 index e8fbd8d..0000000 --- a/scripts/core_registers_example.py +++ /dev/null @@ -1,116 +0,0 @@ -"""Example: construct and parse every core Harp register payload. - -Run with: - uv run python scripts/core_registers_example.py -""" - -from pathlib import Path - -import numpy as np - -from harp.device import ( - # Enums / flags - ClockConfigurationFlags, - EnableFlag, - OperationMode, - ResetFlags, - # Payload classes - OperationControlPayload, - # Registers - AssemblyVersion, - ClockConfiguration, - CoreVersionHigh, - CoreVersionLow, - DeviceName, - FirmwareVersionHigh, - FirmwareVersionLow, - HardwareVersionHigh, - HardwareVersionLow, - OperationControl, - ResetDevice, - SerialNumber, - TimestampMicroseconds, - TimestampSeconds, - WhoAmI, -) - -from harp.data import payload_to_dataframe -from harp.protocol import HarpMessage - -examples = [ - (WhoAmI, np.uint16(1216)), - (TimestampSeconds, np.uint32(3600)), - (TimestampMicroseconds, np.uint16(500)), - ( - OperationControl, - OperationControlPayload( - operation_mode=OperationMode.ACTIVE, - dump_registers=True, - visual_indicators=EnableFlag.ENABLED, - operation_led=EnableFlag.ENABLED, - heartbeat=EnableFlag.ENABLED, - mute_replies=True, - ), - ), - (ResetDevice, ResetFlags.RESTORE_DEFAULT | ResetFlags.RESTORE_NAME), - (DeviceName, "my-harp-device"), - ( - ClockConfiguration, - ClockConfigurationFlags.CLOCK_REPEATER | ClockConfigurationFlags.CLOCK_UNLOCK, - ), - (HardwareVersionHigh, np.uint8(2)), - (HardwareVersionLow, np.uint8(0)), - (AssemblyVersion, np.uint8(3)), - (CoreVersionHigh, np.uint8(1)), - (CoreVersionLow, np.uint8(4)), - (FirmwareVersionHigh, np.uint8(2)), - (FirmwareVersionLow, np.uint8(1)), - (SerialNumber, np.uint16(42)), -] - -print("=== Live round-trip (format → parse) ===") -for register, value in examples: - frame = register.format(value) - parsed = register.parse(HarpMessage.parse(frame)) - print(f"{register.__name__:20s} (addr {register.address:2d}) → {parsed}") - -# --------------------------------------------------------------------------- -# Single-record bitfield access (scalar, no [0] indexing needed) -# --------------------------------------------------------------------------- -print("\n=== Bitfield scalar access ===") -ctrl = OperationControlPayload( - operation_mode=OperationMode.ACTIVE, - heartbeat=EnableFlag.ENABLED, - visual_indicators=EnableFlag.ENABLED, - dump_registers=False, - mute_replies=False, - operation_led=EnableFlag.DISABLED, -) -print(f" operation_mode : {ctrl.operation_mode}") # OperationMode.ACTIVE -print(f" heartbeat : {ctrl.heartbeat}") # EnableFlag.ENABLED -print(f" dump_registers : {ctrl.dump_registers}") # False -print(f" visual_indicators : {ctrl.visual_indicators}") # EnableFlag.ENABLED -print(f" mute_replies : {ctrl.mute_replies}") # False -print(f" operation_led : {ctrl.operation_led}") # EnableFlag.DISABLED - -# --------------------------------------------------------------------------- -# Bulk read from a .bin file (zero-copy, vectorised) -# --------------------------------------------------------------------------- -BIN_FILE = Path( - r"C:\git\bruno-f-cruz\analysis-harlow-learning-sets\data\841312_2026-06-15_19-38-50\behavior\Behavior.harp\Behavior_10.bin" -) -print(f"\n=== Bulk read from {BIN_FILE.name} ===") -_data, timestamps, msg_type, payload = OperationControl.parse_bulk(BIN_FILE.read_bytes()) -print(f" {len(timestamps)} frame(s) read") -print(f" timestamps (s) : {timestamps}") -print(f" operation_mode : {payload.operation_mode}") -print(f" heartbeat : {payload.heartbeat}") -print(f" dump_registers : {payload.dump_registers}") -print(f" visual_indicators : {payload.visual_indicators}") -print(f" mute_replies : {payload.mute_replies}") -print(f" operation_led : {payload.operation_led}") - -print("\n DataFrame:") -df = payload_to_dataframe(payload) -df.insert(0, "timestamp", timestamps) -print(df.to_string(index=False)) diff --git a/scripts/device_integration.py b/scripts/device_integration.py deleted file mode 100644 index 94a6942..0000000 --- a/scripts/device_integration.py +++ /dev/null @@ -1,101 +0,0 @@ -import time - -import numpy as np -from harp.device import ( - AssemblyVersion, - ClockConfiguration, - CoreVersionHigh, - CoreVersionLow, - Device, - FirmwareVersionHigh, - FirmwareVersionLow, - HardwareVersionHigh, - HardwareVersionLow, - OperationControl, - ResetDevice, - SerialNumber, - TimestampMicroseconds, - TimestampSeconds, - WhoAmI, -) -from harp.serial import open_serial_device - -PORT = "COM4" -N_READS = 10_000 - -CORE_REGISTERS = [ - ("WhoAmI", WhoAmI), - ("HardwareVersionHigh", HardwareVersionHigh), - ("HardwareVersionLow", HardwareVersionLow), - ("AssemblyVersion", AssemblyVersion), - ("CoreVersionHigh", CoreVersionHigh), - ("CoreVersionLow", CoreVersionLow), - ("FirmwareVersionHigh", FirmwareVersionHigh), - ("FirmwareVersionLow", FirmwareVersionLow), - ("TimestampSeconds", TimestampSeconds), - ("TimestampMicroseconds", TimestampMicroseconds), - ("OperationControl", OperationControl), - ("ResetDevice", ResetDevice), - ("SerialNumber", SerialNumber), - ("ClockConfiguration", ClockConfiguration), -] - - -def read_core_registers(dev: Device) -> None: - print("\n=== Core register snapshot ===") - print(f"{'Register':<20} {'Address':>7} {'Value'}") - print("-" * 50) - for name, reg in CORE_REGISTERS: - msg = dev.read(reg) - print(f"{name:<20} {reg.address} {msg.parsed}") - - # Bitfield properties return plain Python scalars — no [0] indexing needed - ctrl = dev.read(OperationControl).parsed - print("\n OperationControl breakdown:") - print(f" operation_mode = {ctrl.operation_mode}") - print(f" heartbeat = {ctrl.heartbeat}") - print(f" dump_registers = {ctrl.dump_registers}") - print(f" visual_indicators = {ctrl.visual_indicators}") - - -def latency_benchmark(dev: Device, n: int = N_READS) -> None: - print(f"\n=== WhoAmI round-trip benchmark (n={n:,}) ===") - print("Collecting timestamps …") - - t0 = time.perf_counter() - timestamps = np.empty(n, dtype=np.float64) - for i in range(n): - msg = dev.read(WhoAmI) - timestamps[i] = msg.timestamp - elapsed = time.perf_counter() - t0 - - ts = np.array(timestamps, dtype=np.float64) - if len(ts) < 2: - print("Not enough timestamped replies to compute statistics.") - return - - # Latency proxy: delta between successive device timestamps - deltas_ms = np.diff(ts) * 1e3 # seconds → milliseconds - - print(f"\n Total wall time : {elapsed:.2f} s ({elapsed / n * 1e3:.2f} ms/read)") - print(f" Replies with TS : {len(ts):,} / {n:,}") - print(f"\n Inter-reply delta (ms) [n={len(deltas_ms):,}]") - print(f" min : {deltas_ms.min():.3f}") - print(f" p1 : {np.percentile(deltas_ms, 1):.3f}") - print(f" mean : {deltas_ms.mean():.3f}") - print(f" std : {deltas_ms.std():.3f}") - print(f" p99 : {np.percentile(deltas_ms, 99):.3f}") - print(f" max : {deltas_ms.max():.3f}") - - -# --------------------------------------------------------------------------- -# Entry point -# --------------------------------------------------------------------------- - -if __name__ == "__main__": - # Live device reads (requires hardware) - with open_serial_device(Device, port=PORT) as dev: - sn_message = dev.read(SerialNumber) - sn = sn_message.parsed - read_core_registers(dev) - latency_benchmark(dev) diff --git a/scripts/heterogenous_register.py b/scripts/heterogenous_register.py deleted file mode 100644 index 9f30849..0000000 --- a/scripts/heterogenous_register.py +++ /dev/null @@ -1,92 +0,0 @@ -from typing import ClassVar - -import numpy as np - -from harp.protocol import ( - HarpMessage, - MessageType, - StructPayload, - Field, - StringConverter, - IdentityConverter, - PayloadType, - RegisterBase, -) - -# --------------------------------------------------------------------------- -# Payload -# --------------------------------------------------------------------------- - -# FileSettings0: -# address: 54 -# type: U8 -# access: Write -# length: 45 -# description: "Struct to configure Analog Output Channel 0 -# File Player settings: cycles (U32), duration_us (U32), -# update_frequency_hz (U32), path (U8 array, 33 elements)" - - -class FileSettings0Payload(StructPayload[np.uint8]): - cycles: np.uint32 = Field(converter=IdentityConverter(np.uint32), offset=0) - duration_us: np.uint32 = Field(converter=IdentityConverter(np.uint32), offset=4) - update_frequency_hz: np.uint32 = Field(converter=IdentityConverter(np.uint32), offset=8) - path: str = Field( - converter=StringConverter(33), default="220khzwaveform", offset=12 - ) # defaults work too - - -# --------------------------------------------------------------------------- -# Register -# --------------------------------------------------------------------------- - - -class FileSettings0(RegisterBase[FileSettings0Payload]): - address: ClassVar[int] = 54 - payload_type = PayloadType.U8 - payload_class = FileSettings0Payload - - -# --------------------------------------------------------------------------- -# Round-trip demo -# --------------------------------------------------------------------------- - -if __name__ == "__main__": - # 1. Build payload - payload = FileSettings0Payload( # The stubs for the constructor must be auto generated, but this already works - cycles=np.uint32( - 3 - ), # If you want to be correct, you should use the exact types for the fields, but the converters will handle it if you don't - duration_us=250_000, # however this still works - update_frequency_hz=400, - ) - print(f"Payload dtype : {FileSettings0Payload.dtype}") - print(f"Payload bytes : {payload.raw_payload.tobytes().hex()}") - print(f"Payload : {payload}") - print(f" cycles = {payload.cycles}") - print(f" duration_us = {payload.duration_us}") - print(f" update_frequency_hz = {payload.update_frequency_hz}") - print(f" path = {payload.path!r}") - - # 2. Encode → Harp wire frame - frame = FileSettings0.format(payload, message_type=MessageType.Write) - print(f"\nWire frame ({len(frame)} bytes): {frame.hex()}") - - # 3. Parse wire frame → HarpMessage - msg = HarpMessage.parse(frame) - print(f"\nHarpMessage : {msg}") - - # 4. Parse payload from HarpMessage - parsed = FileSettings0.parse(msg) - print(f"\nParsed payload : {parsed}") - print(f" cycles = {parsed.cycles}") - print(f" duration_us = {parsed.duration_us}") - print(f" update_frequency_hz = {parsed.update_frequency_hz}") - print(f" path = {parsed.path!r}") - - # 5. Assert round-trip integrity - assert parsed.cycles == 3 - assert parsed.duration_us == 250_000 - assert parsed.update_frequency_hz == 400 - assert parsed.path == "220khzwaveform" - print("\nRound-trip OK") diff --git a/uv.lock b/uv.lock index 4254a70..5370bce 100644 --- a/uv.lock +++ b/uv.lock @@ -377,7 +377,6 @@ docs = [ { name = "mkdocs-git-committers-plugin-2" }, { name = "mkdocs-include-markdown-plugin" }, { name = "mkdocs-material" }, - { name = "mkdocs-monorepo-plugin" }, { name = "mkdocstrings-python" }, ] @@ -410,7 +409,6 @@ docs = [ { name = "mkdocs-git-committers-plugin-2", specifier = ">=2.5.0" }, { name = "mkdocs-include-markdown-plugin", specifier = ">=7.1.6" }, { name = "mkdocs-material", specifier = ">=9.6.9" }, - { name = "mkdocs-monorepo-plugin", specifier = ">=1.1.2" }, { name = "mkdocstrings-python", specifier = ">=1.16.6" }, ] @@ -750,19 +748,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, ] -[[package]] -name = "mkdocs-monorepo-plugin" -version = "1.1.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mkdocs" }, - { name = "python-slugify" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d4/6a/a75245020e44beb9d7c806158f8a2cda37597711409d40c5a37c70078a7e/mkdocs-monorepo-plugin-1.1.2.tar.gz", hash = "sha256:09200bcf837ad35070e6da973aa0cb682e69ed6e16f254a30584550c6d2d8ebb", size = 13723, upload-time = "2025-06-05T19:09:45.042Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/57/26/4f4c19457d1d4e6d571a3b092921b7a0ce9477d18d997755ac615d72b96b/mkdocs_monorepo_plugin-1.1.2-py3-none-any.whl", hash = "sha256:4b917bc224b89e34e1736bb31ad5ae9deb0a907da879e03bb9454b41fb8b1cac", size = 14539, upload-time = "2025-06-05T19:09:43.74Z" }, -] - [[package]] name = "mkdocstrings" version = "1.0.4" @@ -1051,18 +1036,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] -[[package]] -name = "python-slugify" -version = "8.0.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "text-unidecode" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/87/c7/5e1547c44e31da50a460df93af11a535ace568ef89d7a811069ead340c4a/python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856", size = 10921, upload-time = "2024-02-08T18:32:45.488Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051, upload-time = "2024-02-08T18:32:43.911Z" }, -] - [[package]] name = "pyyaml" version = "6.0.3" @@ -1188,15 +1161,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, ] -[[package]] -name = "text-unidecode" -version = "1.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93", size = 76885, upload-time = "2019-08-30T21:36:45.405Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154, upload-time = "2019-08-30T21:37:03.543Z" }, -] - [[package]] name = "tomli" version = "2.4.1" From 70df2ee3920538492e520ed92a495271e49a8d70 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 17 Jul 2026 11:25:19 -0700 Subject: [PATCH 255/267] Improve package metadata --- .github/workflows/pyharp.yml | 15 +- .gitignore | 1 + .pre-commit-config.yaml | 22 - mkdocs.yml | 3 - mypy.ini | 14 - pyproject.toml | 42 +- scripts/_harp_io.py | 148 --- scripts/benchmark_analog_data.py | 189 ---- src/harp/py.typed | 0 src/packages/harp-benchmarks/pyproject.toml | 2 +- .../src/harp/benchmarks/py.typed | 0 src/packages/harp-data/README.md | 12 +- src/packages/harp-data/pyproject.toml | 2 +- src/packages/harp-data/src/harp/data/py.typed | 0 src/packages/harp-device/pyproject.toml | 2 +- src/packages/harp-protocol/pyproject.toml | 4 +- src/packages/harp-serial/pyproject.toml | 2 +- .../harp-serial/src/harp/serial/py.typed | 0 tests/test_device.py | 247 ----- uv.lock | 892 +++++++++--------- 20 files changed, 504 insertions(+), 1093 deletions(-) delete mode 100644 .pre-commit-config.yaml delete mode 100644 mypy.ini delete mode 100644 scripts/_harp_io.py delete mode 100644 scripts/benchmark_analog_data.py create mode 100644 src/harp/py.typed create mode 100644 src/packages/harp-benchmarks/src/harp/benchmarks/py.typed create mode 100644 src/packages/harp-data/src/harp/data/py.typed create mode 100644 src/packages/harp-serial/src/harp/serial/py.typed delete mode 100644 tests/test_device.py diff --git a/.github/workflows/pyharp.yml b/.github/workflows/pyharp.yml index 18a01cc..40b3a80 100644 --- a/.github/workflows/pyharp.yml +++ b/.github/workflows/pyharp.yml @@ -17,7 +17,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ["3.10", "3.11", "3.12", "3.13"] + python-version: ["3.11", "3.12", "3.13"] fail-fast: false steps: - uses: actions/checkout@v7 @@ -73,8 +73,17 @@ jobs: - name: Validate version format run: uv version ${{ steps.get_version.outputs.version }} --dry-run - - name: Set version - run: uv version ${{ steps.get_version.outputs.version }} + - name: Set version across all workspace packages + shell: bash + run: | + version="${{ steps.get_version.outputs.version }}" + # Discover every workspace member (root + src/packages/*) by its declared + # package name so all versions move in lock-step with the release tag. + mapfile -t pkgs < <(grep -h '^name = ' pyproject.toml src/packages/*/pyproject.toml | sed -E 's/^name = "(.*)"/\1/') + for pkg in "${pkgs[@]}"; do + echo "Setting $pkg -> $version" + uv version --package "$pkg" "$version" + done - name: Build run: uv build --all-packages diff --git a/.gitignore b/.gitignore index 40bba35..691e07d 100644 --- a/.gitignore +++ b/.gitignore @@ -175,5 +175,6 @@ cython_debug/ # vscode .vscode/ + # Internal benchmark artifacts (harp-benchmarks) /benchmark/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 23d8811..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,22 +0,0 @@ -repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: "v5.0.0" - hooks: - - id: check-case-conflict - - id: check-merge-conflict - - id: check-toml - - id: check-yaml - - id: check-json - exclude: ^.devcontainer/devcontainer.json - - id: pretty-format-json - exclude: ^.devcontainer/devcontainer.json - args: [--autofix, --no-sort-keys] - - id: end-of-file-fixer - - id: trailing-whitespace - - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.11.5" - hooks: - - id: ruff - args: [--exit-non-zero-on-fix] - - id: ruff-format diff --git a/mkdocs.yml b/mkdocs.yml index 9d6b086..c420ad3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -18,9 +18,6 @@ plugins: show_source: false extensions: - griffe_fieldz - - git-committers: - repository: harp-tech/pyharp - branch: main - git-authors diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 779054c..0000000 --- a/mypy.ini +++ /dev/null @@ -1,14 +0,0 @@ - -[mypy] -#strict = True -namespace_packages = True -warn_redundant_casts = True -warn_unused_ignores = True -disallow_subclassing_any = True -disallow_untyped_calls = True -#disallow_untyped_defs = True -#check_untyped_defs = True -warn_return_any = True -no_implicit_optional = False -strict_optional = True -ignore_missing_imports = True diff --git a/pyproject.toml b/pyproject.toml index c522e9e..1001721 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,34 @@ [project] name = "harp" -version = "0.2.0" +version = "0.0.0" description = "Library for data acquisition and control of devices implementing the Harp protocol." authors = [{ name = "harp-tech", email = "contact@harp-tech.org" }] license = "MIT" +license-files = ["LICENSE"] readme = 'README.md' -keywords = ['python', 'harp'] -requires-python = ">=3.10,<4.0" +keywords = [ + "harp", + "harp-protocol", + "data-acquisition", + "serial", + "hardware", + "device-control", +] +requires-python = ">=3.11" +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Science/Research", + "Intended Audience :: Developers", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX :: Linux", + "Operating System :: MacOS", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Scientific/Engineering", + "Topic :: System :: Hardware :: Hardware Drivers", +] dependencies = [ "harp-protocol", "harp-device", @@ -15,9 +37,20 @@ dependencies = [ ] [project.urls] +Homepage = "https://harp-tech.org/" Repository = "https://github.com/harp-tech/pyharp/" -"Bug Tracker" = "https://github.com/harp-tech/pyharp/issues" Documentation = "https://harp-tech.org/pyharp/" +"Bug Tracker" = "https://github.com/harp-tech/pyharp/issues" +Changelog = "https://github.com/harp-tech/pyharp/releases" + +[build-system] +requires = ["uv_build>=0.9.5"] +build-backend = "uv_build" + +[tool.uv.build-backend] +module-name = "harp" +module-root = "src" +namespace = true [tool.uv.sources] harp-protocol = { workspace = true } @@ -37,7 +70,6 @@ docs = [ "mkdocs>=1.6.1", "mkdocs-codeinclude-plugin>=0.2.1", "mkdocs-git-authors-plugin>=0.9.4", - "mkdocs-git-committers-plugin-2>=2.5.0", "mkdocs-include-markdown-plugin>=7.1.6", "mkdocs-material>=9.6.9", "mkdocstrings-python>=1.16.6", diff --git a/scripts/_harp_io.py b/scripts/_harp_io.py deleted file mode 100644 index caaa283..0000000 --- a/scripts/_harp_io.py +++ /dev/null @@ -1,148 +0,0 @@ -"""Verbatim copy of harp-python/harp/io.py `read` function. - -Source: https://github.com/harp-tech/harp-python/blob/main/harp/io.py -Vendored here for the benchmark so we don't depend on the harp-python package -being installed. Only `read` is included; `to_file`/`to_buffer` are omitted. - -_BufferLike / _FileLike are inlined from harp/typing.py to keep this file -self-contained. -""" - -import mmap -import sys -from datetime import datetime -from enum import IntEnum -from os import PathLike -from typing import Any, BinaryIO, Optional, Union - -import numpy as np -import numpy.typing as npt -import pandas as pd - -# --------------------------------------------------------------------------- -# Types (inlined from harp/typing.py) -# --------------------------------------------------------------------------- - -if sys.version_info >= (3, 12): - from collections.abc import Buffer as _BufferLike -else: - _BufferLike = Union[bytes, bytearray, memoryview, mmap.mmap, npt.NDArray[Any]] - -_FileLike = Union[str, "PathLike[str]", BinaryIO] - -# --------------------------------------------------------------------------- -# Constants -# --------------------------------------------------------------------------- - -REFERENCE_EPOCH = datetime(1904, 1, 1) - -_SECONDS_PER_TICK = 32e-6 -_PAYLOAD_TIMESTAMP_MASK = 0x10 - - -class MessageType(IntEnum): - NA = 0 - READ = 1 - WRITE = 2 - EVENT = 3 - - -_messagetypes = [t.name for t in MessageType] - -_dtypefrompayloadtype = { - 1: np.dtype(np.uint8), - 2: np.dtype(np.uint16), - 4: np.dtype(np.uint32), - 8: np.dtype(np.uint64), - 129: np.dtype(np.int8), - 130: np.dtype(np.int16), - 132: np.dtype(np.int32), - 136: np.dtype(np.int64), - 68: np.dtype(np.float32), -} - -# --------------------------------------------------------------------------- -# Public API -# --------------------------------------------------------------------------- - - -def read( - file_or_buf: Union[_FileLike, _BufferLike], - address: Optional[int] = None, - dtype: Optional[np.dtype] = None, - length: Optional[int] = None, - columns: Optional[list] = None, - epoch: Optional[datetime] = None, - keep_type: bool = False, -) -> pd.DataFrame: - """Read single-register Harp data from the specified file or buffer. - - Returns a pandas DataFrame with a ``Time`` index (seconds as float64, or - datetime if ``epoch`` is given). Column order matches the payload layout. - """ - if isinstance(file_or_buf, (str, PathLike, BinaryIO)) or hasattr(file_or_buf, "readinto"): - data = np.fromfile(file_or_buf, dtype=np.uint8) # type: ignore[arg-type] - else: - data = np.frombuffer(file_or_buf, dtype=np.uint8) # type: ignore[arg-type] - - if len(data) == 0: - return pd.DataFrame( - columns=columns, - index=( - pd.DatetimeIndex([], name="Time") - if epoch - else pd.Index([], dtype=np.float64, name="Time") - ), - ) - - if address is not None and address != data[2]: - raise ValueError(f"expected address {address} but got {data[2]}") - - stride = int(data[1]) + 2 - nrows = len(data) // stride - payloadtype = int(data[4]) - payloadoffset = 5 - index = None - - if payloadtype & _PAYLOAD_TIMESTAMP_MASK: - seconds = np.ndarray( - nrows, dtype=np.uint32, buffer=data, offset=payloadoffset, strides=stride - ) - payloadoffset += 4 - micros = np.ndarray( - nrows, dtype=np.uint16, buffer=data, offset=payloadoffset, strides=stride - ) - payloadoffset += 2 - time = micros * _SECONDS_PER_TICK + seconds - payloadtype = payloadtype & ~_PAYLOAD_TIMESTAMP_MASK - if epoch is not None: - time = epoch + pd.to_timedelta(time, "s") # type: ignore[assignment] - index = pd.DatetimeIndex(time) - index.name = "Time" - else: - index = pd.Series(time) - index.name = "Time" - - payloadsize = stride - payloadoffset - 1 - payload_dtype = _dtypefrompayloadtype[payloadtype] - if dtype is not None and dtype != payload_dtype: - raise ValueError(f"expected payload type {dtype} but got {payload_dtype}") - - elementsize = payload_dtype.itemsize - payloadshape = (nrows, payloadsize // elementsize) - if length is not None and length != payloadshape[1]: - raise ValueError(f"expected payload length {length} but got {payloadshape[1]}") - - payload = np.ndarray( - payloadshape, - dtype=payload_dtype, - buffer=data, - offset=payloadoffset, - strides=(stride, elementsize), - ) - - result = pd.DataFrame(payload, index=index, columns=columns) - if keep_type: - msgtype = np.ndarray(nrows, dtype=np.uint8, buffer=data, offset=0, strides=stride) - result[MessageType.__name__] = pd.Categorical.from_codes(msgtype, categories=_messagetypes) - return result diff --git a/scripts/benchmark_analog_data.py b/scripts/benchmark_analog_data.py deleted file mode 100644 index 3a563eb..0000000 --- a/scripts/benchmark_analog_data.py +++ /dev/null @@ -1,189 +0,0 @@ -"""Benchmark: pyharp read_frames vs harp-python read for AnalogData (reg 44). - -AnalogData — address 44, S16 array of length 3, named fields: - analog_input0, encoder, analog_input1 - -The benchmark compares two strategies for decoding ~3.8M frames (~65 MB) of -AnalogData from a Harp binary file into a pandas DataFrame with named columns: - - harp-python strategy: - harp_io.read(file, columns=[...]) → DataFrame directly - - pyharp strategy: - AnalogData.parse_bulk(file) → (..., timestamps, ..., payload) - harp.data.to_dataframe(payload) → DataFrame with named columns - -Both are zero-copy for the payload bytes (strided np.ndarray views into the raw -file buffer). The harp-python path builds the DataFrame in one shot; the pyharp -path splits parsing from presentation. - -Run with: - uv run python scripts/benchmark_analog_data.py -""" - -import sys -import timeit -from pathlib import Path -from typing import ClassVar - -import numpy as np - -sys.path.insert(0, str(Path(__file__).parent.parent)) -from scripts._harp_io import read as harp_read # vendored harp-python read() - -from harp.data import parse_to_dataframe, payload_to_dataframe -from harp.protocol import PayloadBase, Field, PayloadType, RegisterBase, IdentityConverter - - -REPO_ROOT = Path(__file__).parent.parent - -# --------------------------------------------------------------------------- -# AnalogData register definition (hand-written; would come from codegen) -# --------------------------------------------------------------------------- - -ANALOG_COLUMNS = ["analog_input0", "encoder", "analog_input1"] - - -class AnalogDataPayload(PayloadBase[np.int16], length=3): - """Payload for AnalogData (register 44): three signed 16-bit channels.""" - - analog_input0 = Field(converter=IdentityConverter(np.int16), offset=0) - encoder = Field(converter=IdentityConverter(np.int16), offset=1) - analog_input1 = Field(converter=IdentityConverter(np.int16), offset=2) - - -class AnalogData(RegisterBase[AnalogDataPayload]): - address: ClassVar[int] = 44 - payload_type: ClassVar[PayloadType] = PayloadType.S16 - payload_class: ClassVar[type[AnalogDataPayload]] = AnalogDataPayload - # length = None → structured dtype; read_frames will treat it as 1 element - # of size 6 bytes, squeezing to shape (N,). - - -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- - -BIN_FILE = Path(r"C:\Users\bruno.cruz\Downloads\Behavior_44.bin") - - -def pyharp_read(path: Path, *, include_timestamp: bool = True): - """pyharp path: parse_bulk → to_dataframe.""" - bytes_2_parse = path.read_bytes() - _data, timestamps, msg_type, payload = AnalogData.parse_bulk(bytes_2_parse) - df = payload_to_dataframe(payload) - if include_timestamp: - df.insert(0, "timestamp", timestamps) - return df - - -def pyharp_read_dataframe(path: Path, *, timestamp: bool = True): - """pyharp one-call path: read_dataframe.""" - return parse_to_dataframe(AnalogData, path.read_bytes(), timestamp=timestamp) - - -def harp_python_read(path: Path): - """harp-python path: read() directly to DataFrame.""" - return harp_read(path, columns=ANALOG_COLUMNS) - - -# --------------------------------------------------------------------------- -# Sanity check: both paths must produce the same values -# --------------------------------------------------------------------------- - - -def sanity_check() -> None: - df_pyharp = pyharp_read(BIN_FILE) - df_harp = harp_python_read(BIN_FILE) - - assert len(df_pyharp) == len(df_harp), ( - f"row count mismatch: pyharp={len(df_pyharp)}, harp-python={len(df_harp)}" - ) - for col in ANALOG_COLUMNS: - np.testing.assert_array_equal( - df_pyharp[col].to_numpy(), - df_harp[col].to_numpy(), - err_msg=f"column {col!r} differs", - ) - print(f" Sanity check passed — {len(df_pyharp):,} rows, columns match.") - - -# --------------------------------------------------------------------------- -# Benchmark -# --------------------------------------------------------------------------- - -N_REPEATS = 100 -N_LOOPS = 1 - - -def benchmark(label: str, fn, path: Path) -> np.ndarray: - times = np.array(timeit.repeat(lambda: fn(path), repeat=N_REPEATS, number=N_LOOPS)) - print( - f" {label:<45s} " - f"min={times.min():.3f}s " - f"mean={times.mean():.3f}s " - f"max={times.max():.3f}s " - f"(n={N_REPEATS})" - ) - return times - - -if __name__ == "__main__": - if not BIN_FILE.exists(): - print(f"ERROR: {BIN_FILE} not found. Place the Behavior_44.bin file there.") - sys.exit(1) - - file_mb = BIN_FILE.stat().st_size / 1024**2 - - # Quick probe for frame count - data = np.fromfile(BIN_FILE, dtype=np.uint8) - stride = int(data[1]) + 2 - nrows = len(data) // stride - del data # free before benchmarks - - print("\n=== AnalogData benchmark ===") - print(f" File : {BIN_FILE.name} ({file_mb:.1f} MB)") - print(f" Frames: {nrows:,} | stride={stride} bytes\n") - - print("Sanity check:") - sanity_check() - - print(f"\nBenchmark ({N_REPEATS} repeats, 1 call each):") - t_harp = benchmark("harp-python read()", harp_python_read, BIN_FILE) - t_py_ts = benchmark("pyharp read_frames()+to_dataframe()+ts", pyharp_read, BIN_FILE) - t_py_no_ts = benchmark( - "pyharp read_frames()+to_dataframe() (no ts)", - lambda p: pyharp_read(p, include_timestamp=False), - BIN_FILE, - ) - t_py_rd = benchmark( - "pyharp read_dataframe(timestamp=True)", - pyharp_read_dataframe, - BIN_FILE, - ) - t_py_rd_no_ts = benchmark( - "pyharp read_dataframe(timestamp=False)", - lambda p: pyharp_read_dataframe(p, timestamp=False), - BIN_FILE, - ) - - print("") - for label, t_py in [ - ("read_frames + to_dataframe + ts", t_py_ts), - ("read_frames + to_dataframe (no ts)", t_py_no_ts), - ("read_dataframe(timestamp=True)", t_py_rd), - ("read_dataframe(timestamp=False)", t_py_rd_no_ts), - ]: - ratio_min = t_py.min() / t_harp.min() - ratio_mean = t_py.mean() / t_harp.mean() - direction = "slower" if ratio_mean > 1 else "faster" - print( - f" {label:<40s} vs harp-python :" - f" min={ratio_min:.2f}x mean={ratio_mean:.2f}x" - f" ({direction} by {abs(ratio_mean - 1) * 100:.0f}% on mean)" - ) - - # Show a sample of the result - print("\nSample output (pyharp, first 3 rows):") - df = pyharp_read(BIN_FILE) - print(df.head(3).to_string(index=False)) diff --git a/src/harp/py.typed b/src/harp/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/packages/harp-benchmarks/pyproject.toml b/src/packages/harp-benchmarks/pyproject.toml index 96f54cb..9caa8f0 100644 --- a/src/packages/harp-benchmarks/pyproject.toml +++ b/src/packages/harp-benchmarks/pyproject.toml @@ -19,7 +19,7 @@ harp-benchmark = "harp.benchmarks.benchmark:main" harp-benchmark-generate = "harp.benchmarks.generate:main" [build-system] -requires = ["uv_build>=0.9.5,<0.10.0"] +requires = ["uv_build>=0.9.5"] build-backend = "uv_build" [tool.uv.build-backend] diff --git a/src/packages/harp-benchmarks/src/harp/benchmarks/py.typed b/src/packages/harp-benchmarks/src/harp/benchmarks/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/packages/harp-data/README.md b/src/packages/harp-data/README.md index d489be0..abfdbde 100644 --- a/src/packages/harp-data/README.md +++ b/src/packages/harp-data/README.md @@ -6,15 +6,15 @@ pandas-free `ColumnData` view that this package assembles into a DataFrame. ## Read a register from a file -`read_dataframe` takes a register and a source (path, bytes, or open binary +`parse_to_dataframe` takes a register and a source (path, bytes, or open binary file) and returns one row per frame: ```python -from harp.data import read_dataframe +from harp.data import parse_to_dataframe from my_device import AnalogData -df = read_dataframe(AnalogData, "AnalogData.bin") -df = read_dataframe(AnalogData, raw, timestamp=True, message_type=False, decode_enums=True) +df = parse_to_dataframe(AnalogData, "AnalogData.bin") +df = parse_to_dataframe(AnalogData, raw, timestamp=True, message_type=False, decode_enums=True) ``` Enum fields decode to `pd.Categorical` (`decode_enums=False` keeps raw codes). @@ -25,8 +25,8 @@ If you already have a batched payload (e.g. from `register.parse_bulk`), convert it directly: ```python -from harp.data import to_dataframe +from harp.data import payload_to_dataframe _data, timestamps, _msg, payload = AnalogData.parse_bulk(raw) -df = to_dataframe(payload) +df = payload_to_dataframe(payload) ``` diff --git a/src/packages/harp-data/pyproject.toml b/src/packages/harp-data/pyproject.toml index d7e34d5..38f8fab 100644 --- a/src/packages/harp-data/pyproject.toml +++ b/src/packages/harp-data/pyproject.toml @@ -10,7 +10,7 @@ dependencies = [ ] [build-system] -requires = ["uv_build>=0.9.5,<0.10.0"] +requires = ["uv_build>=0.9.5"] build-backend = "uv_build" [tool.uv.build-backend] diff --git a/src/packages/harp-data/src/harp/data/py.typed b/src/packages/harp-data/src/harp/data/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/packages/harp-device/pyproject.toml b/src/packages/harp-device/pyproject.toml index 608854e..3101f3c 100644 --- a/src/packages/harp-device/pyproject.toml +++ b/src/packages/harp-device/pyproject.toml @@ -8,7 +8,7 @@ dependencies = [ ] [build-system] -requires = ["uv_build>=0.9.5,<0.10.0"] +requires = ["uv_build>=0.9.5"] build-backend = "uv_build" [tool.uv.build-backend] diff --git a/src/packages/harp-protocol/pyproject.toml b/src/packages/harp-protocol/pyproject.toml index 5147dde..f129a69 100644 --- a/src/packages/harp-protocol/pyproject.toml +++ b/src/packages/harp-protocol/pyproject.toml @@ -5,14 +5,14 @@ description = "Library with the base types for Harp protocol usage." authors = [{ name = "harp-tech", email = "contact@harp-tech.org" }] license = "MIT" keywords = ['python', 'harp'] -requires-python = ">=3.10,<4.0" +requires-python = ">=3.11" dependencies = [ "numpy>=1.24", "typing-extensions>=4.0", ] [build-system] -requires = ["uv_build>=0.9.5,<0.10.0"] +requires = ["uv_build>=0.9.5"] build-backend = "uv_build" [tool.uv.build-backend] diff --git a/src/packages/harp-serial/pyproject.toml b/src/packages/harp-serial/pyproject.toml index c9d1b1d..4bc7480 100644 --- a/src/packages/harp-serial/pyproject.toml +++ b/src/packages/harp-serial/pyproject.toml @@ -10,7 +10,7 @@ dependencies = [ ] [build-system] -requires = ["uv_build>=0.9.5,<0.10.0"] +requires = ["uv_build>=0.9.5"] build-backend = "uv_build" [tool.uv.build-backend] diff --git a/src/packages/harp-serial/src/harp/serial/py.typed b/src/packages/harp-serial/src/harp/serial/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_device.py b/tests/test_device.py deleted file mode 100644 index b49452d..0000000 --- a/tests/test_device.py +++ /dev/null @@ -1,247 +0,0 @@ -"""Tests for the device layer: descriptors, to_dataframe, and read_frames.""" - -import enum -from typing import ClassVar - -import numpy as np - -from harp.data import payload_to_dataframe -from harp.protocol._builder import build_message_frame -from harp.protocol._message_type import MessageType -from harp.protocol._payload import PayloadBase, BitMask, GroupMask -from harp.protocol._payload_type import PayloadType - -from harp.device._registers import ( - EnableFlag, - OperationControl, - OperationControlPayload, - OperationMode, -) - - -class Pins(enum.IntFlag): - PIN0 = 0x01 - PIN1 = 0x02 - PIN2 = 0x04 - PIN3 = 0x08 - PIN4 = 0x10 - PIN5 = 0x20 - PIN6 = 0x40 - PIN7 = 0x80 - - -class PinsPayload(PayloadBase[np.uint8]): - pins = BitMask(enum=Pins, mask=0xFF) - - -# --- Minimal fixture payload class ------------------------------------------ - - -class _FlagPayload(PayloadBase[np.uint8]): - _dtype: ClassVar = np.dtype("u1") - _repr_fields: ClassVar = ("flag", "group") - - flag = BitMask(enum=Pins, mask=0x01) - group = GroupMask(mask=0x06, enum=OperationMode) - - -# --- BitMask behaviour ------------------------------------------------------ - - -def test_bitmask_single_returns_flag_set(): - p = _FlagPayload.from_buffer(bytes([0x01])) - assert p.flag == Pins.PIN0 - assert isinstance(p.flag, Pins) - - -def test_bitmask_single_returns_empty_flag(): - p = _FlagPayload.from_buffer(bytes([0x00])) - assert p.flag == Pins(0) - assert isinstance(p.flag, Pins) - - -def test_bitmask_batch_returns_raw_int_ndarray(): - p = _FlagPayload.from_buffer(bytes([0x01, 0x00, 0x01])) - result = p.flag - assert isinstance(result, np.ndarray) - np.testing.assert_array_equal(result, [1, 0, 1]) - - -# --- GroupMask behaviour ----------------------------------------------------- - - -def test_groupmask_single_returns_enum(): - p = _FlagPayload.from_buffer(bytes([0x02])) # bits 1-2 = 01 -> Active - result = p.group - assert result == OperationMode.ACTIVE - assert isinstance(result, OperationMode) - - -def test_groupmask_batch_returns_ndarray(): - p = _FlagPayload.from_buffer(bytes([0x00, 0x02, 0x06])) # groups: 0, 1, 3 - result = p.group - assert isinstance(result, np.ndarray) - np.testing.assert_array_equal(result, [0, 1, 3]) - - -# --- OperationControlPayload ------------------------------------------------- - - -def _make_op_ctrl_byte( - mode: OperationMode = OperationMode.STANDBY, - heartbeat: EnableFlag = EnableFlag.DISABLED, -) -> int: - val = int(mode) & 0x03 - val |= (int(heartbeat) & 0x01) << 7 - return val - - -def test_op_ctrl_scalar_from_buffer(): - val = _make_op_ctrl_byte(OperationMode.ACTIVE, EnableFlag.ENABLED) - p = OperationControlPayload.from_buffer(bytes([val])) - assert p.operation_mode == OperationMode.ACTIVE - assert p.heartbeat == EnableFlag.ENABLED - assert p.dump_registers is False - - -def test_op_ctrl_init_matches_from_buffer(): - p_init = OperationControlPayload( - operation_mode=OperationMode.ACTIVE, heartbeat=EnableFlag.ENABLED - ) - val = _make_op_ctrl_byte(OperationMode.ACTIVE, EnableFlag.ENABLED) - p_buf = OperationControlPayload.from_buffer(bytes([val])) - assert p_init.operation_mode == p_buf.operation_mode - assert p_init.heartbeat == p_buf.heartbeat - - -def test_op_ctrl_batch_descriptor_returns_ndarray(): - vals = [ - _make_op_ctrl_byte(OperationMode.ACTIVE, EnableFlag.ENABLED), - _make_op_ctrl_byte(OperationMode.STANDBY, EnableFlag.DISABLED), - ] - p = OperationControlPayload.from_buffer(bytes(vals)) - assert isinstance(p.heartbeat, np.ndarray) - np.testing.assert_array_equal(p.heartbeat, [True, False]) - - -def test_op_ctrl_to_dataframe(): - vals = [ - _make_op_ctrl_byte(OperationMode.ACTIVE, EnableFlag.ENABLED), - _make_op_ctrl_byte(OperationMode.STANDBY, EnableFlag.DISABLED), - ] - p = OperationControlPayload.from_buffer(bytes(vals)) - df = payload_to_dataframe(p, decode_enums=False) - assert list(df.columns) == list(OperationControlPayload._repr_fields) - assert len(df) == 2 - np.testing.assert_array_equal(df["heartbeat"], [True, False]) - np.testing.assert_array_equal( - df["operation_mode"], - [int(OperationMode.ACTIVE), int(OperationMode.STANDBY)], - ) - - -# --- PinsPayload (cuttlefish) ------------------------------------------------ - - -def test_pins_single_scalar(): - p = PinsPayload.from_buffer(bytes([0b00000101])) - assert Pins.PIN0 in p.pins - assert Pins.PIN1 not in p.pins - assert Pins.PIN2 in p.pins - assert Pins.PIN7 not in p.pins - - -def test_pins_batch_raw_int(): - p = PinsPayload.from_buffer(bytes([0b00000001, 0b00000010])) - np.testing.assert_array_equal(p.pins, [0b01, 0b10]) - - -def test_pins_to_dataframe_single_column(): - p = PinsPayload.from_buffer(bytes([0b00000101, 0b00000010])) - df = payload_to_dataframe(p) - assert list(df.columns) == ["pins"] - np.testing.assert_array_equal(df["pins"], [0b101, 0b10]) - assert len(df) == 2 - - -def test_pins_to_dataframe_demuxed(): - p = PinsPayload.from_buffer(bytes([0b00000101, 0b00000010])) - df = payload_to_dataframe(p, demux_bit_masks=True) - assert list(df.columns) == [m.name for m in Pins] - np.testing.assert_array_equal(df["PIN0"], [True, False]) - np.testing.assert_array_equal(df["PIN1"], [False, True]) - np.testing.assert_array_equal(df["PIN2"], [True, False]) - - -# --- read_frames round-trip -------------------------------------------------- - - -def _make_frames(values: list, base_time: float = 1.0) -> bytes: - """Build a raw binary buffer of N timestamped OperationControl frames.""" - frames = b"" - for i, v in enumerate(values): - frames += build_message_frame( - MessageType.Read, - address=OperationControl.address, - payload_type=PayloadType.U8, - payload=bytes([v]), - timestamp=base_time + i, - ) - return frames - - -def _read_frames(raw: bytes): - """Adapter over the current bulk API: returns (timestamps, payload).""" - _data, timestamps, _msgtype, payload = OperationControl.parse_bulk(raw) - if timestamps is None: - timestamps = np.empty(0, dtype=np.float64) - return timestamps, payload - - -def test_read_frames_count(): - raw = _make_frames([0x01, 0x00, 0x81]) - timestamps, payload = _read_frames(raw) - assert len(timestamps) == 3 - assert len(payload) == 3 - - -def test_read_frames_timestamps(): - raw = _make_frames([0x01, 0x00, 0x81], base_time=10.0) - timestamps, _ = _read_frames(raw) - np.testing.assert_allclose(timestamps, [10.0, 11.0, 12.0], atol=1e-4) - - -def test_read_frames_payload_type(): - raw = _make_frames([0x01]) - _, payload = _read_frames(raw) - assert isinstance(payload, OperationControlPayload) - - -def test_read_frames_bitfield_batch(): - vals = [ - _make_op_ctrl_byte(OperationMode.ACTIVE, EnableFlag.ENABLED), - _make_op_ctrl_byte(OperationMode.STANDBY, EnableFlag.DISABLED), - ] - raw = _make_frames(vals) - _, payload = _read_frames(raw) - np.testing.assert_array_equal(payload.heartbeat, [True, False]) - np.testing.assert_array_equal( - payload.operation_mode, - [int(OperationMode.ACTIVE), int(OperationMode.STANDBY)], - ) - - -def test_read_frames_to_dataframe(): - vals = [_make_op_ctrl_byte(OperationMode.ACTIVE), _make_op_ctrl_byte(), _make_op_ctrl_byte()] - raw = _make_frames(vals) - _, payload = _read_frames(raw) - df = payload_to_dataframe(payload) - assert len(df) == 3 - assert "heartbeat" in df.columns - assert "operation_mode" in df.columns - - -def test_read_frames_empty(): - timestamps, payload = _read_frames(b"") - assert len(timestamps) == 0 - assert len(payload) == 0 diff --git a/uv.lock b/uv.lock index 5370bce..7f9820e 100644 --- a/uv.lock +++ b/uv.lock @@ -1,13 +1,16 @@ version = 1 revision = 3 -requires-python = ">=3.11, <4.0" +requires-python = ">=3.11" resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.12' and sys_platform == 'emscripten'", + "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] [manifest] @@ -31,135 +34,119 @@ wheels = [ [[package]] name = "backrefs" -version = "6.2" +version = "7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/a6/e325ec73b638d3ede4421b5445d4a0b8b219481826cc079d510100af356c/backrefs-6.2.tar.gz", hash = "sha256:f44ff4d48808b243b6c0cdc6231e22195c32f77046018141556c66f8bab72a49", size = 7012303, upload-time = "2026-02-16T19:10:15.828Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/a7dd63622beef68cc0d3c3c36d472e143dd95443d5ebf14cd1a5b4dfbf11/backrefs-7.0.tar.gz", hash = "sha256:4989bb9e1e99eb23647c7160ed51fb21d0b41b5d200f2d3017da41e023097e82", size = 7012453, upload-time = "2026-04-28T16:28:04.215Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/39/3765df263e08a4df37f4f43cb5aa3c6c17a4bdd42ecfe841e04c26037171/backrefs-6.2-py310-none-any.whl", hash = "sha256:0fdc7b012420b6b144410342caeb8adc54c6866cf12064abc9bb211302e496f8", size = 381075, upload-time = "2026-02-16T19:10:04.322Z" }, - { url = "https://files.pythonhosted.org/packages/0f/f0/35240571e1b67ffb19dafb29ab34150b6f59f93f717b041082cdb1bfceb1/backrefs-6.2-py311-none-any.whl", hash = "sha256:08aa7fae530c6b2361d7bdcbda1a7c454e330cc9dbcd03f5c23205e430e5c3be", size = 392874, upload-time = "2026-02-16T19:10:06.314Z" }, - { url = "https://files.pythonhosted.org/packages/e3/63/77e8c9745b4d227cce9f5e0a6f68041278c5f9b18588b35905f5f19c1beb/backrefs-6.2-py312-none-any.whl", hash = "sha256:c3f4b9cb2af8cda0d87ab4f57800b57b95428488477be164dd2b47be54db0c90", size = 398787, upload-time = "2026-02-16T19:10:08.274Z" }, - { url = "https://files.pythonhosted.org/packages/c5/71/c754b1737ad99102e03fa3235acb6cb6d3ac9d6f596cbc3e5f236705abd8/backrefs-6.2-py313-none-any.whl", hash = "sha256:12df81596ab511f783b7d87c043ce26bc5b0288cf3bb03610fe76b8189282b2b", size = 400747, upload-time = "2026-02-16T19:10:09.791Z" }, - { url = "https://files.pythonhosted.org/packages/af/75/be12ba31a6eb20dccef2320cd8ccb3f7d9013b68ba4c70156259fee9e409/backrefs-6.2-py314-none-any.whl", hash = "sha256:e5f805ae09819caa1aa0623b4a83790e7028604aa2b8c73ba602c4454e665de7", size = 412602, upload-time = "2026-02-16T19:10:12.317Z" }, - { url = "https://files.pythonhosted.org/packages/21/f8/d02f650c47d05034dcd6f9c8cf94f39598b7a89c00ecda0ecb2911bc27e9/backrefs-6.2-py39-none-any.whl", hash = "sha256:664e33cd88c6840b7625b826ecf2555f32d491800900f5a541f772c485f7cda7", size = 381077, upload-time = "2026-02-16T19:10:13.74Z" }, + { url = "https://files.pythonhosted.org/packages/d4/39/39a31d7eae729ea14ed10c3ccef79371197177b9355a86cb3525709e8502/backrefs-7.0-py310-none-any.whl", hash = "sha256:b57cd227ea556b0aed3dc9b8da4628db4eabc0402c6d7fcfc69283a93955f7e9", size = 380824, upload-time = "2026-04-28T16:27:55.647Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b5/9302644225ba7dfa934a2ff2b9c7bb85701313a90dddb3dfaf693fa5bae2/backrefs-7.0-py311-none-any.whl", hash = "sha256:a0fa7360c63509e9e077e174ef4e6d3c21c8db94189b9d957289ae6d794b9475", size = 392626, upload-time = "2026-04-28T16:27:57.42Z" }, + { url = "https://files.pythonhosted.org/packages/36/da/87912ddec6e06feffbaa3d7aa18fc6352bee2e8f1fee185d7d1690f8f4e8/backrefs-7.0-py312-none-any.whl", hash = "sha256:ca42ce6a49ace3d75684dfa9937f3373902a63284ecb385ce36d15e5dcb41c12", size = 398537, upload-time = "2026-04-28T16:27:58.913Z" }, + { url = "https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl", hash = "sha256:f2c52955d631b9e1ac4cd56209f0a3a946d592b98e7790e77699339ae01c102a", size = 400491, upload-time = "2026-04-28T16:28:00.928Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5c/fb93d3092640a24dfb7bd7727a24016d7c01774ca013e60efd3f683c8002/backrefs-7.0-py314-none-any.whl", hash = "sha256:a6448b28180e3ca01134c9cf09dcebafad8531072e09903c5451748a05f24bc9", size = 412349, upload-time = "2026-04-28T16:28:02.412Z" }, ] [[package]] name = "bracex" -version = "2.6" +version = "3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/63/9a/fec38644694abfaaeca2798b58e276a8e61de49e2e37494ace423395febc/bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7", size = 26642, upload-time = "2025-06-22T19:12:31.254Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/f5/4473ad9b48cd0420a2d762a3750fa0e078e23e060b1af72662e5987e5530/bracex-3.0.tar.gz", hash = "sha256:b73f718d6bd98d8419e45df02426c86e9967c179949f779340d6c3a8c83b9111", size = 43162, upload-time = "2026-06-30T00:43:35.279Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508, upload-time = "2025-06-22T19:12:29.781Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2e/68781b78e764e5ccc4af1e3d27e060069c73af90234853fa80000e7ee79d/bracex-3.0-py3-none-any.whl", hash = "sha256:3833e61c2f092d5aa0468fa2e6c6e990a306185abf763b6d122f0158e59c58a5", size = 11738, upload-time = "2026-06-30T00:43:34.196Z" }, ] [[package]] name = "certifi" -version = "2026.4.22" +version = "2026.6.17" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, ] [[package]] name = "charset-normalizer" -version = "3.4.7" +version = "3.4.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, - { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, - { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, - { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, - { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, - { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, - { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, - { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, - { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, - { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, - { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, - { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, - { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, - { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, - { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, - { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, - { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, - { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, - { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, - { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, - { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, - { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, - { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, - { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, - { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, - { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, - { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, - { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, - { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, - { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, - { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, - { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, - { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, - { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, - { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, - { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, - { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, - { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, - { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, - { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, - { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, - { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, - { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, - { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, - { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, - { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, - { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, - { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, - { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, - { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, - { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, - { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, - { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, - { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, - { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, - { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, - { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, - { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, - { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, - { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, - { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, - { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, - { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, - { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, - { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, - { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, - { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, - { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, - { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, - { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, - { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" }, + { url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" }, + { url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" }, + { url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" }, + { url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" }, + { url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" }, + { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, + { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, + { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, + { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, + { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, + { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, + { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, + { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, ] [[package]] name = "click" -version = "8.3.3" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, ] [[package]] @@ -173,101 +160,86 @@ wheels = [ [[package]] name = "coverage" -version = "7.13.5" +version = "7.15.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/d0/55fe630f4cf94e3fcba868240fad8c8cdd1f764e2a932f8926347e6ec4cd/coverage-7.15.2.tar.gz", hash = "sha256:3df60dc267f0a2ca23cb7a9ab1109c62b9335ffbf519fcfe167157c28c09b81d", size = 927741, upload-time = "2026-07-15T18:56:19.558Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d", size = 219381, upload-time = "2026-03-17T10:30:14.68Z" }, - { url = "https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587", size = 219880, upload-time = "2026-03-17T10:30:16.231Z" }, - { url = "https://files.pythonhosted.org/packages/55/2f/e0e5b237bffdb5d6c530ce87cc1d413a5b7d7dfd60fb067ad6d254c35c76/coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642", size = 250303, upload-time = "2026-03-17T10:30:17.748Z" }, - { url = "https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b", size = 252218, upload-time = "2026-03-17T10:30:19.804Z" }, - { url = "https://files.pythonhosted.org/packages/da/69/2f47bb6fa1b8d1e3e5d0c4be8ccb4313c63d742476a619418f85740d597b/coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686", size = 254326, upload-time = "2026-03-17T10:30:21.321Z" }, - { url = "https://files.pythonhosted.org/packages/d5/d0/79db81da58965bd29dabc8f4ad2a2af70611a57cba9d1ec006f072f30a54/coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743", size = 256267, upload-time = "2026-03-17T10:30:23.094Z" }, - { url = "https://files.pythonhosted.org/packages/e5/32/d0d7cc8168f91ddab44c0ce4806b969df5f5fdfdbb568eaca2dbc2a04936/coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75", size = 250430, upload-time = "2026-03-17T10:30:25.311Z" }, - { url = "https://files.pythonhosted.org/packages/4d/06/a055311d891ddbe231cd69fdd20ea4be6e3603ffebddf8704b8ca8e10a3c/coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209", size = 252017, upload-time = "2026-03-17T10:30:27.284Z" }, - { url = "https://files.pythonhosted.org/packages/d6/f6/d0fd2d21e29a657b5f77a2fe7082e1568158340dceb941954f776dce1b7b/coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a", size = 250080, upload-time = "2026-03-17T10:30:29.481Z" }, - { url = "https://files.pythonhosted.org/packages/4e/ab/0d7fb2efc2e9a5eb7ddcc6e722f834a69b454b7e6e5888c3a8567ecffb31/coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e", size = 253843, upload-time = "2026-03-17T10:30:31.301Z" }, - { url = "https://files.pythonhosted.org/packages/ba/6f/7467b917bbf5408610178f62a49c0ed4377bb16c1657f689cc61470da8ce/coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd", size = 249802, upload-time = "2026-03-17T10:30:33.358Z" }, - { url = "https://files.pythonhosted.org/packages/75/2c/1172fb689df92135f5bfbbd69fc83017a76d24ea2e2f3a1154007e2fb9f8/coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8", size = 250707, upload-time = "2026-03-17T10:30:35.2Z" }, - { url = "https://files.pythonhosted.org/packages/67/21/9ac389377380a07884e3b48ba7a620fcd9dbfaf1d40565facdc6b36ec9ef/coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf", size = 221880, upload-time = "2026-03-17T10:30:36.775Z" }, - { url = "https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9", size = 222816, upload-time = "2026-03-17T10:30:38.891Z" }, - { url = "https://files.pythonhosted.org/packages/12/a6/1d3f6155fb0010ca68eba7fe48ca6c9da7385058b77a95848710ecf189b1/coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028", size = 221483, upload-time = "2026-03-17T10:30:40.463Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, - { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, - { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, - { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, - { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, - { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, - { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, - { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, - { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, - { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, - { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, - { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, - { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, - { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, - { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, - { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, - { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, - { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, - { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, - { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, - { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, - { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, - { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, - { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, - { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, - { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, - { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, - { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, - { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, - { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, - { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, - { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, - { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, - { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, - { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, - { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, - { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, - { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, - { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, - { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, - { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, - { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, - { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, - { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, - { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, - { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, - { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, - { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, - { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, - { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, - { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, - { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, - { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, - { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, - { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, - { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, - { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, - { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, - { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, - { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, - { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, - { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, - { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, - { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, - { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, - { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, - { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, - { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, + { url = "https://files.pythonhosted.org/packages/7d/3a/54536704f507d4573bf9161c4d0dd3dd59b6d85e48c664e901b6844d8e33/coverage-7.15.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f1ec6f304b156669cfde653b4e9a953f5de87e247ea02ac599bce0ab2744036", size = 221414, upload-time = "2026-07-15T18:53:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d9/8ba925d29743e3577b21e4d8c11a702b76bc93c41e7fdfd1177af63d4b8d/coverage-7.15.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d3361879d736f469f45723c11ea1a5bbdaf1f6928f0e632c940378b5aa9b660", size = 221913, upload-time = "2026-07-15T18:53:53.682Z" }, + { url = "https://files.pythonhosted.org/packages/09/54/a855f3aa0187f2b431ade4e4791b77b56282cfb5d201c83ec26a31b5b36a/coverage-7.15.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c6a98d698f9e2c8008d0370ec7fc452ebfcc530002ae2d0061170d768b992589", size = 252332, upload-time = "2026-07-15T18:53:55.467Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d3/13ac97b4370640ba3452fc8559b06cc2f479ce3ba4a0b632a73e44c38a7d/coverage-7.15.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d50dd325e18ec25bfcc10cd7f99b04df1ab9ec76b0918c260e60817ad0643dee", size = 254243, upload-time = "2026-07-15T18:53:57.055Z" }, + { url = "https://files.pythonhosted.org/packages/88/83/5eca144942d8d0659d3f55176517f4a59cdc65eefd17146a0770935a3ebd/coverage-7.15.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67d7602480a47bdf5b675635403625553ebaa70d5a62a657c035149fd401cea0", size = 256352, upload-time = "2026-07-15T18:53:58.83Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ba/d3db2e01a50fc88cdb4c0f19542bcf6f61489e34dc9aa3538413e2459a38/coverage-7.15.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cee0f89f4767a6057c8fbf168f8135f18be651300496086bd873e3189fed0487", size = 258313, upload-time = "2026-07-15T18:54:00.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/b3/aba83416e9177df28e5186d856c19158c59fc0e7e814aaa61a4a2354ad1b/coverage-7.15.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a29ec5305a7335aacee2d799e3422e91e1c8a12474986e2b3b07e315c91be82f", size = 252449, upload-time = "2026-07-15T18:54:02.456Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a5/4b00ecac0194431ab451b0f6710f8e2517d04cef60f821b14dec4637d575/coverage-7.15.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:48ccc6395958eda89093ecdc35644c86f23a8b23a7f4d44958812b721aad67c1", size = 254043, upload-time = "2026-07-15T18:54:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/75/b6/cfa209b4313ee7f1b34da47efcd789ea51c024ad35af390e00f5a3c10a2e/coverage-7.15.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:81f382c5a94b434ec1f6da607edb904c76d7212e618cd4d1bc9f97bed4120ef5", size = 252107, upload-time = "2026-07-15T18:54:06.745Z" }, + { url = "https://files.pythonhosted.org/packages/36/67/e8cac5a6954038c98d7fe7eb9802afe7ab3ecb637bb7cc00e69b4148b56d/coverage-7.15.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bbc808daf4f5cd567af8075ecc72d21c6dfef9a254709a621a84c217c935ebc0", size = 255873, upload-time = "2026-07-15T18:54:08.48Z" }, + { url = "https://files.pythonhosted.org/packages/2c/92/395cca9f330a86c3fe3471d73e2c102116c4c58fdc619dbbc125c6e93a54/coverage-7.15.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a4c46b247b5d4b78f613bd89fea926d32b25c6cc61a50bd1e99ba310348f3dad", size = 251826, upload-time = "2026-07-15T18:54:10.083Z" }, + { url = "https://files.pythonhosted.org/packages/51/60/3e91b20295439652424f426b7086ec5bf4fbe3f604c73eda22b986c4fd6b/coverage-7.15.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:094dd37f3ef7b2da8b068b583d1f4c40f91c65197e16c52a71962d5d537fc5db", size = 252735, upload-time = "2026-07-15T18:54:11.878Z" }, + { url = "https://files.pythonhosted.org/packages/a5/eb/8c07839005e5e3c6b3877d3a6e2a80ce766589f31dd2b6882b78d59a7b8c/coverage-7.15.2-cp311-cp311-win32.whl", hash = "sha256:a63b9e190711134d581c4d703df5df09851b1acf99792c7aacbbe9f41f0283c9", size = 223500, upload-time = "2026-07-15T18:54:13.525Z" }, + { url = "https://files.pythonhosted.org/packages/2e/98/59d83c257cd59f0fbaf9d9ddb26b744a576760dfd1ae16e516408894a02b/coverage-7.15.2-cp311-cp311-win_amd64.whl", hash = "sha256:8bb9f4b4279187560796a4cdaca3b0a93dd97e48ee667df005f4ed9a97403688", size = 223973, upload-time = "2026-07-15T18:54:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/ea/09/2d285c8bef5c4f695d120c1c96dc11715638aa8e134069f210bb6a62a9fe/coverage-7.15.2-cp311-cp311-win_arm64.whl", hash = "sha256:8c726b232659cbd2ae57ade46509eb068c9bd7a06df9fcbff6fe484870006934", size = 223519, upload-time = "2026-07-15T18:54:16.803Z" }, + { url = "https://files.pythonhosted.org/packages/6a/50/eb5bf42e531611a9f8d272556b1ed4de503f84a91413584094487cf69f8f/coverage-7.15.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1adac78e5abc7c5438f7a209c9ca69d06542f0bf481d728b6989ea80b813fdf9", size = 221587, upload-time = "2026-07-15T18:54:18.439Z" }, + { url = "https://files.pythonhosted.org/packages/06/d1/da99af464c335d4e023a6efcd7ec30f63b88a43c93745154ab74ffb31cea/coverage-7.15.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b868acc62aa5de3be7a9d05c2333bf8359ca987e43f9cb30ff8fbda6a024ab73", size = 221943, upload-time = "2026-07-15T18:54:20.062Z" }, + { url = "https://files.pythonhosted.org/packages/5b/8a/13c42723d61ca447eafa18732e8141dd6a63f2732e1c7e1502c182dd88d7/coverage-7.15.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6f6966fc30e6f06ca8f98fb0ce51eda6b111b3ee8d066a8b1ec9e77fa06ab55d", size = 253450, upload-time = "2026-07-15T18:54:21.765Z" }, + { url = "https://files.pythonhosted.org/packages/d7/29/99021303f98fbdcb63504b4d07bea4cc025b9b2dd907c4f07c85d50a0dab/coverage-7.15.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:68af907f595ab01a78f794932ff3bdf929c316d3000810d38dbc247129e26f8b", size = 256187, upload-time = "2026-07-15T18:54:23.4Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a8/fd503715ed6ca9c5d742923aa5209257340b367a867b2ced0c7d4ba8a0b9/coverage-7.15.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:afa29e2eff3d5729267e2cb2fd4ce9d61c952932fb2694e34ccb5d9540c6a296", size = 257301, upload-time = "2026-07-15T18:54:25.183Z" }, + { url = "https://files.pythonhosted.org/packages/da/40/3f4b8fb409810036ebc2857d36adc0498c6e957b5df0290c5036b2e143f1/coverage-7.15.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bbf44513ceb1589e31948e20eafbde9deaface90e1a1afa5f5f77b4423d17ce6", size = 259562, upload-time = "2026-07-15T18:54:27.204Z" }, + { url = "https://files.pythonhosted.org/packages/0b/8a/9bdffbef47db77cce3d6b02a28f7e919b19f0106c4b080c2c2246040f885/coverage-7.15.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9deddf09eecb717b7f980414b43d90a5b22ff3967d2949ab29cb0aa83d9e9098", size = 253841, upload-time = "2026-07-15T18:54:29.134Z" }, + { url = "https://files.pythonhosted.org/packages/1b/1e/9031efde019d31a06646261fce6dfc5c3c74e951e27a71e5c9a424563178/coverage-7.15.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae901f7e55ba405c84ee1cab3d3e962e4e871e4a2bcb9c90911adbd69b42ac5a", size = 255221, upload-time = "2026-07-15T18:54:31.142Z" }, + { url = "https://files.pythonhosted.org/packages/56/db/787acde872389fc84a9ef9d8cd1ccc658e391ab4cb5b28092a714426a394/coverage-7.15.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a0f47002c6eeb7c280228467a4cb0cc15ca2103a8421b986b2d3ec04a0f9bd8b", size = 253366, upload-time = "2026-07-15T18:54:32.886Z" }, + { url = "https://files.pythonhosted.org/packages/2f/9b/6f57bc4b93c842eef1695f8cdaf2318e35e7ba54f5ba80d84be213ab7858/coverage-7.15.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd7a5beb7af3e864a13b1f0fb26efd3695da43ef0daf71e586adfffaf34d5b2", size = 257434, upload-time = "2026-07-15T18:54:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/88/26/b3186a21b2acc83e451118978905c81c7072c3333707804db09a78c096a2/coverage-7.15.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:97a5c5457a9fb1d6c4e06cfb5dc835871fbfb6a6a51addc9e925bdeff5ef7440", size = 252935, upload-time = "2026-07-15T18:54:36.548Z" }, + { url = "https://files.pythonhosted.org/packages/20/c2/c9f3376b2e717ea69ed7a6e9a5fcab968fb0b290db6cf4bd9a1fc7541b75/coverage-7.15.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0901cfe6c13bcd2302da4f83e884555d2a22bda6e4c476f09ef204ba20ca536e", size = 254807, upload-time = "2026-07-15T18:54:38.296Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e1/dfc15401f4a8aaeb486e1ba3e9e3c40522a6e38bd0ecf0b3f29cb8082957/coverage-7.15.2-cp312-cp312-win32.whl", hash = "sha256:b171bdd71cb7ff792bf32e376173b0ace7e7963e7e57c58dfc42063a6a7174cd", size = 223641, upload-time = "2026-07-15T18:54:40.103Z" }, + { url = "https://files.pythonhosted.org/packages/91/40/81b6d809d320cd366ec5bdf8176575e897dcb8efe7fb4b489ef9e93e4d13/coverage-7.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:582edc45c2040543fef83341be23c43024a3ab3ae0c2d8bc498a06282905ad40", size = 224172, upload-time = "2026-07-15T18:54:41.882Z" }, + { url = "https://files.pythonhosted.org/packages/ef/28/9f14ec438149f7de557f45518f09b4a7917b795cc37083aa7db482693f8c/coverage-7.15.2-cp312-cp312-win_arm64.whl", hash = "sha256:a638db90c61cd219aeee65e83a24fdaa57269a741ae0cf773309208ac862cee3", size = 223556, upload-time = "2026-07-15T18:54:43.674Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d5/f8c838e6b7282976f7c918884b792df7a0c42c5bba5d99c60ad2d221d56d/coverage-7.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1121caa19159a38b5463eaae4b1e1fde81e525b15ecc5e000cd5b1a108f743a8", size = 221606, upload-time = "2026-07-15T18:54:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/bf/37/97c926376364f66298cc44893b89cdf17b8bc406376497c4061ae4b8a8ff/coverage-7.15.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a300c6934e0989c327b9e8a1e110329da4641149f872bbe9f70168be66da76c1", size = 221982, upload-time = "2026-07-15T18:54:47.341Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/a36050a6e83c2135ee0776f452ca3948224befc6d7f26acecc082d0c106a/coverage-7.15.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2617f8799d268fabdeef42a7e89ac3a23e1deee9025427db2df970f99a89a578", size = 252972, upload-time = "2026-07-15T18:54:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/31/d3/06b5f1daf95f0f15ab05bd75f26ba5f3c8b33d0bb72f3aaa3cf41d1bad3a/coverage-7.15.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7dc2950a2992cd676d35c20ae63522836deeb034f08874699d14068710af3dc1", size = 255569, upload-time = "2026-07-15T18:54:51.098Z" }, + { url = "https://files.pythonhosted.org/packages/81/1c/9afb3f8de2b8d36960391c48559a2e3ff96594b58099f115921549ea8d0d/coverage-7.15.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e36686f7a442185db2400b3df171aac520869faf9deb59df687d28659eda2a6", size = 256806, upload-time = "2026-07-15T18:54:53.145Z" }, + { url = "https://files.pythonhosted.org/packages/64/d8/b989f96061a5e32d82fddd1b1b9ff48a7c8f8ae7606f0e80fd9de54b1e33/coverage-7.15.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d29ca7bd67af6e12e74632d65f026eabc1364da5c254494cd914446a28a3ef7", size = 258936, upload-time = "2026-07-15T18:54:55.015Z" }, + { url = "https://files.pythonhosted.org/packages/b8/fa/f99771f5110457c7b511c1935ca49ddf288218eaa84322e028b9334146ae/coverage-7.15.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:db9c8438057e5b0f6a22a0af99c0c1d26b57fbbdbd1be5861ddb8f897fcc3a2d", size = 253178, upload-time = "2026-07-15T18:54:57.527Z" }, + { url = "https://files.pythonhosted.org/packages/f6/96/c098a6044d119c751ceede7be91035fa8310170ec24a6523aff72f0a5793/coverage-7.15.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:63022c4c8dec1d0342f05c3ede99842fe3d007689acc45e86f123a1746e4a026", size = 254934, upload-time = "2026-07-15T18:54:59.41Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a2/1457b3a7a50c8d77500103b97a046db863e2f59a1cf6d2f814595f349885/coverage-7.15.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6c0be82b4d4aa5b2704e08518e2252f3e3d110164bcca826816801052e48a7aa", size = 252898, upload-time = "2026-07-15T18:55:01.338Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0e/76958874c471ecfcdde0d2b2747bb2c61bdbf34a40636f4ce9db9923e643/coverage-7.15.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4510fb9cdf6bb02dfa6af0be4a534b8102d086e22e4a33f8836df663da3d660d", size = 257056, upload-time = "2026-07-15T18:55:03.243Z" }, + { url = "https://files.pythonhosted.org/packages/7c/7c/3d7c4e3bf58baa40327dc7edc2272b17cf02299366d52763db1b0ca1556a/coverage-7.15.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:42ec3d989421b174a2ab607c1539f24127ad362757b7f1c0c0d7a2993f7eb37b", size = 252718, upload-time = "2026-07-15T18:55:05.029Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b8/1cecffed9ce14fb25be9ba42d37b6bb61485c9a3ddd43cd3dde36b6087d8/coverage-7.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8f91bce78e32343af184c3b7fa28fcf5a9e2641f4b6623d392038f804939188", size = 254490, upload-time = "2026-07-15T18:55:06.889Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2c/42984561bc7f4c045dca67516a0c50ee5ef8d84352dbeb5559dc86c4823e/coverage-7.15.2-cp313-cp313-win32.whl", hash = "sha256:434e68d531858205895eb0d74b73d20b84260de426387d53c422a5acda2cf050", size = 223647, upload-time = "2026-07-15T18:55:08.941Z" }, + { url = "https://files.pythonhosted.org/packages/41/9f/39c7c9245efc583beddf89a87683574e663ed93637f3afb6cd7b88405676/coverage-7.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:26c3b04a6377fd7c09800921fa934e3a17c0020439cd59df73e73ae1d4b6a78c", size = 224190, upload-time = "2026-07-15T18:55:10.789Z" }, + { url = "https://files.pythonhosted.org/packages/c7/de/3a2883cf8a213659280ef4b403059e17a9acaeb7fc7fd4105e1226ff2e6d/coverage-7.15.2-cp313-cp313-win_arm64.whl", hash = "sha256:3ed010aa1b69cda8e827aabfca9866216c980e2dca82ab9a78c5f83689964c8b", size = 223583, upload-time = "2026-07-15T18:55:12.678Z" }, + { url = "https://files.pythonhosted.org/packages/81/5f/aed265fd7a3551a394f36dfe41868aee709b7f95db4052205b4ad1563ac3/coverage-7.15.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:40f633c5c5fc783732f6312280122e859538fa24461235597c13d803ea9a108a", size = 221650, upload-time = "2026-07-15T18:55:14.527Z" }, + { url = "https://files.pythonhosted.org/packages/6b/2c/222ba12a545189017120f8eddfc1a0bd4616b47d5d4a8d99421edb2fe4c6/coverage-7.15.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:075560438765b7a2ef43bf7aa7758661b53d889df47f062a31bda6c1ade553a2", size = 221988, upload-time = "2026-07-15T18:55:16.674Z" }, + { url = "https://files.pythonhosted.org/packages/aa/38/304b5877ab46e6c290b4292cfcf3fe28245f0e5597cad7f6acc91fc7e0a4/coverage-7.15.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:25fd15dd40a0a2c51a500d664ca29053c09c3259d998407bf982b6e114696138", size = 253029, upload-time = "2026-07-15T18:55:18.856Z" }, + { url = "https://files.pythonhosted.org/packages/6c/58/821b533b8db9e44cf1d8a97bd525149ced40dde1d0093da02cb78e715244/coverage-7.15.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f", size = 255536, upload-time = "2026-07-15T18:55:21.027Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f2/7aa06604c389d32ea7f0a6a988359a7eafc3cd3f8e7bc2e88cd2fdf0b877/coverage-7.15.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9854ca62c152874b2060772503535be2e8f53f70b8aaa7686b094888d872f984", size = 256881, upload-time = "2026-07-15T18:55:23.125Z" }, + { url = "https://files.pythonhosted.org/packages/a2/4f/1ef342339c7916d0096bc5888cc0f653882cc7bc8f897d5cb89143287c9b/coverage-7.15.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:913b6c56e110da40e035bbd168353bf7aaa2544a5eaccea5d98a4629aac156c7", size = 259196, upload-time = "2026-07-15T18:55:25.099Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f4/7ed055d7a9c5ec13b161773a115a5ccc6b0081d568c31fad830806306cc7/coverage-7.15.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aaccad4129d735a8a4d526f26929894c9a4e8ef7034566f210b176749d6906e3", size = 253036, upload-time = "2026-07-15T18:55:27.018Z" }, + { url = "https://files.pythonhosted.org/packages/14/79/ea82cca18c242a3a38b6c017da39726aa62dcb64aa635abf79b92009975c/coverage-7.15.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a164b50081fc7357331c4024ef4d17b78ba325f8380d05f5a69599a7e05257ee", size = 254887, upload-time = "2026-07-15T18:55:29.084Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ba/a136db3c0d9562b00e10b72540dbf3a33cd3bc5b95060c9308e247494623/coverage-7.15.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:bfd341ccf78128e72c094bc70cc25b3ef309c33c7c2c66ba3ed4309549e02de1", size = 252852, upload-time = "2026-07-15T18:55:31.184Z" }, + { url = "https://files.pythonhosted.org/packages/17/17/ea334246b16b7d059953fad6fdefa11e33c68efbd3fe37b1098120a1fac2/coverage-7.15.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1473b3ba8e7ee0f076117b1a72c23f579a2b9e2bb742f48a8d86ea27ca93f91a", size = 257128, upload-time = "2026-07-15T18:55:33.163Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c3/074fb66d46d607855f710876b117cbda562c5ab08363528e78820449f937/coverage-7.15.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:17c432b5f73ad52ef46fb06019f6fa7c66ce381961cf0f7dfd1d3a4bd3a98145", size = 252668, upload-time = "2026-07-15T18:55:35.063Z" }, + { url = "https://files.pythonhosted.org/packages/e1/c1/f620850ada9b36435921c9a3a8057013422b1d964eb4bf37fe138724d192/coverage-7.15.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:77f0ef5011df53a4bd1b35211ab122287f8d9b8d7aa1c4553e5c2deb24b1d446", size = 254325, upload-time = "2026-07-15T18:55:37.125Z" }, + { url = "https://files.pythonhosted.org/packages/cc/31/a729ca3689404493af82ef8e6ff70bd88bdda8da89aeef6ca9b387aeb2b4/coverage-7.15.2-cp314-cp314-win32.whl", hash = "sha256:f653e5d7248c1191ec988a85c72edeab46c3ff44f90639a4ed4874ec0be90243", size = 223844, upload-time = "2026-07-15T18:55:39.078Z" }, + { url = "https://files.pythonhosted.org/packages/c6/83/5d809dc808fb1698c671f3e372259bb9158e64b7ea526fc6ab7de64de9fe/coverage-7.15.2-cp314-cp314-win_amd64.whl", hash = "sha256:9911f31aad8906abe337c271343485cf20df5e70df5d2f57f9f136e7b55f26bc", size = 224331, upload-time = "2026-07-15T18:55:41.346Z" }, + { url = "https://files.pythonhosted.org/packages/16/4e/35e488548e952795829e129995c4174df33bf432b591d1aa42c8d9e4e7ad/coverage-7.15.2-cp314-cp314-win_arm64.whl", hash = "sha256:e38def96ad59853824c97953fdcd2c320a84ba3ce99b417db78af8bb6c3db635", size = 223760, upload-time = "2026-07-15T18:55:43.518Z" }, + { url = "https://files.pythonhosted.org/packages/ed/49/dd2c86cd6374038f6e415fb5bfb86db5218553209c081384a020369dee79/coverage-7.15.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:835ec4e20b45f0a7f63ed78f94065aca00de033403df8377bfe8b9c6abc0a7be", size = 222384, upload-time = "2026-07-15T18:55:45.569Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/173ff17a1c0808e5a438f549f6f145d5ac7528f2791310b63523e3200ac7/coverage-7.15.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7466cc7ab6dc0db871d264bf99e8779f0917ee63d40730af0552f71535a6e072", size = 222647, upload-time = "2026-07-15T18:55:47.544Z" }, + { url = "https://files.pythonhosted.org/packages/84/f8/b8cba872162356fb44ac79c10309d987206a4461e32072fc29228dad7331/coverage-7.15.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e370c12133095ff18432de8c044962be85a5a96d90c6fcbce8e17e76236d2328", size = 264013, upload-time = "2026-07-15T18:55:49.768Z" }, + { url = "https://files.pythonhosted.org/packages/ee/67/a807a7586d0b8cae485308ddd55756f0806c92f8e0b411bacbf23c48edf3/coverage-7.15.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fe41909c9515c3bfdb5f02c4d1f857dba322d9a9a1178069b91eea77889df63a", size = 266135, upload-time = "2026-07-15T18:55:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/67/cd78771dc985f7e4ebdcc82b1a96d9a932af9e806f01f2f91a89f4c72e80/coverage-7.15.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6aa28cfb6488e5453b5b762d65f73aa586380f6693a04d58078ce228a29b06c0", size = 268555, upload-time = "2026-07-15T18:55:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/18/3e/10134cf81275188c58568f324fc74aedff32c63ca4d5bbc513a91944a6f0/coverage-7.15.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcc0aae933921d03096f53b0b03eeb702129fd406dee59f08d2efacc68681fa5", size = 269674, upload-time = "2026-07-15T18:55:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/75/4a/771b77de446cba985dc414bbc5844bd21604da05dbc044286df8318a48a7/coverage-7.15.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7c63387e21ab21f512c69c9756a8c7dadd322c7275edb064064433c9a09c3743", size = 263101, upload-time = "2026-07-15T18:55:58.107Z" }, + { url = "https://files.pythonhosted.org/packages/5f/b5/70a7011da15f4071943361183aefa27847f3e3aec4fd335f1cb3d3a622b1/coverage-7.15.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e55510bc98ae943cece9e667a6c0fe94c6a92913720dea34243657a17993d0c", size = 266007, upload-time = "2026-07-15T18:56:00.468Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0d/f9547e804ce7ad49646ffeffac26699510efbe6c0f751b66fdc960c4e825/coverage-7.15.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2ff08701be2d1556fc78b326c80a3e8042da09352ecb3819105f8e386c8a3071", size = 263611, upload-time = "2026-07-15T18:56:02.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/59/f576a396659c0efd351f5c1544f67c3560e89c7761cabf7f65e412beeda5/coverage-7.15.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:38c9518b7103826c403a461544e3c2e77151e8676d06eaed85911a97e962584a", size = 267344, upload-time = "2026-07-15T18:56:04.622Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5d/c2e4fce3579c0cb635024293f1a32bbe26df101b3e3a69f22243d1352b6c/coverage-7.15.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:dee88b1ed88587abd8c0269a1fc1f4cc77f7750d1dfde2869e2a123af420e67d", size = 262456, upload-time = "2026-07-15T18:56:06.641Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/956287d69436b66094bc4b57ac2da71e43bfd2a5524e958900b9f582fcf8/coverage-7.15.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fbeeeecea279727f8ac16c8e1133ddfeee793e985c86ae343d6a5ce744eef8c", size = 264771, upload-time = "2026-07-15T18:56:08.795Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5a/6f979530c2734c575de77cf58f5f28d51f7123a94b5030fd9156fe5f363c/coverage-7.15.2-cp314-cp314t-win32.whl", hash = "sha256:cb0fddaa6884be6aae36ced9544b5e90f7d5f03845a2853bf47a14953a4e8688", size = 224151, upload-time = "2026-07-15T18:56:10.856Z" }, + { url = "https://files.pythonhosted.org/packages/54/7e/27f6b2a74d484742f4017553e710b01e396b23d809df3e95ca0bb9a2824b/coverage-7.15.2-cp314-cp314t-win_amd64.whl", hash = "sha256:77f091ea3a9cc611cd29f433565476bc1936c084ac8eee00ea0e7e70c27e4199", size = 224981, upload-time = "2026-07-15T18:56:12.928Z" }, + { url = "https://files.pythonhosted.org/packages/b1/48/284863423aa474240f6842bd00d680da22f4e6ea2e466618ef7c9c9e69a9/coverage-7.15.2-cp314-cp314t-win_arm64.whl", hash = "sha256:6fc448c377d6eeb00a47c673494bd9bae29280ca53987e1869e67ebedfe20658", size = 224294, upload-time = "2026-07-15T18:56:15.156Z" }, + { url = "https://files.pythonhosted.org/packages/ec/82/32e3bd191d498e64f6f911ad55d14006a0861e54869d2d32452326399e65/coverage-7.15.2-py3-none-any.whl", hash = "sha256:eb6bcae8d1a9d305351ecb108232441d11c5cfe9de840a04388ba5d2db8d735c", size = 213375, upload-time = "2026-07-15T18:56:17.305Z" }, ] [package.optional-dependencies] @@ -299,30 +271,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, ] -[[package]] -name = "gitdb" -version = "4.0.12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "smmap" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, -] - -[[package]] -name = "gitpython" -version = "3.1.47" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "gitdb" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c1/bd/50db468e9b1310529a19fce651b3b0e753b5c07954d486cba31bbee9a5d5/gitpython-3.1.47.tar.gz", hash = "sha256:dba27f922bd2b42cb54c87a8ab3cb6beb6bf07f3d564e21ac848913a05a8a3cd", size = 216978, upload-time = "2026-04-22T02:44:44.059Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/c5/a1bc0996af85757903cf2bf444a7824e68e0035ce63fb41d6f76f9def68b/gitpython-3.1.47-py3-none-any.whl", hash = "sha256:489f590edfd6d20571b2c0e72c6a6ac6915ee8b8cd04572330e3842207a78905", size = 209547, upload-time = "2026-04-22T02:44:41.271Z" }, -] - [[package]] name = "griffe-fieldz" version = "0.5.0" @@ -338,17 +286,17 @@ wheels = [ [[package]] name = "griffelib" -version = "2.0.2" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/e4/8d187ea29c2e30b3a09505c567513077d6117861bde1fbd997a167f262ec/griffelib-2.1.0.tar.gz", hash = "sha256:762a186d2c6fd6794d4ea20d428d597ffb857cb56b66421651cbba15bdd5e813", size = 216234, upload-time = "2026-06-19T12:05:42.278Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl", hash = "sha256:cc7b3d2d2865ad0b909fcc38086e3f554b5ea7acbaa7bbb7ecaa3f5dfb7d9f00", size = 142560, upload-time = "2026-06-19T12:05:38.742Z" }, ] [[package]] name = "harp" -version = "0.2.0" -source = { virtual = "." } +version = "0.0.0" +source = { editable = "." } dependencies = [ { name = "harp-data" }, { name = "harp-device" }, @@ -374,7 +322,6 @@ docs = [ { name = "mkdocs" }, { name = "mkdocs-codeinclude-plugin" }, { name = "mkdocs-git-authors-plugin" }, - { name = "mkdocs-git-committers-plugin-2" }, { name = "mkdocs-include-markdown-plugin" }, { name = "mkdocs-material" }, { name = "mkdocstrings-python" }, @@ -406,7 +353,6 @@ docs = [ { name = "mkdocs", specifier = ">=1.6.1" }, { name = "mkdocs-codeinclude-plugin", specifier = ">=0.2.1" }, { name = "mkdocs-git-authors-plugin", specifier = ">=0.9.4" }, - { name = "mkdocs-git-committers-plugin-2", specifier = ">=2.5.0" }, { name = "mkdocs-include-markdown-plugin", specifier = ">=7.1.6" }, { name = "mkdocs-material", specifier = ">=9.6.9" }, { name = "mkdocstrings-python", specifier = ">=1.16.6" }, @@ -419,7 +365,8 @@ source = { editable = "src/packages/harp-benchmarks" } dependencies = [ { name = "harp-data" }, { name = "harp-protocol" }, - { name = "numpy" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "pandas" }, ] @@ -437,7 +384,8 @@ version = "0.1.0" source = { editable = "src/packages/harp-data" } dependencies = [ { name = "harp-protocol" }, - { name = "numpy" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "pandas" }, ] @@ -464,7 +412,8 @@ name = "harp-protocol" version = "0.4.0" source = { editable = "src/packages/harp-protocol" } dependencies = [ - { name = "numpy" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "typing-extensions" }, ] @@ -493,11 +442,11 @@ requires-dist = [ [[package]] name = "idna" -version = "3.13" +version = "3.18" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] [[package]] @@ -690,31 +639,17 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/bc/a4166201c2789657c4d370bfcd71a5107edec185ae245675c8b9a6719243/mkdocs_git_authors_plugin-0.10.0-py3-none-any.whl", hash = "sha256:28421a99c3e872a8e205674bb80ec48524838243e5f59eaf9bd97df103e38901", size = 21899, upload-time = "2025-06-10T05:42:39.244Z" }, ] -[[package]] -name = "mkdocs-git-committers-plugin-2" -version = "2.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "gitpython" }, - { name = "mkdocs" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b4/8a/4ca4fb7d17f66fa709b49744c597204ad03fb3b011c76919564843426f11/mkdocs_git_committers_plugin_2-2.5.0.tar.gz", hash = "sha256:a01f17369e79ca28651681cddf212770e646e6191954bad884ca3067316aae60", size = 15183, upload-time = "2025-01-30T07:30:48.667Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/f5/768590251839a148c188d64779b809bde0e78a306295c18fc29d7fc71ce1/mkdocs_git_committers_plugin_2-2.5.0-py3-none-any.whl", hash = "sha256:1778becf98ccdc5fac809ac7b62cf01d3c67d6e8432723dffbb823307d1193c4", size = 11788, upload-time = "2025-01-30T07:30:45.748Z" }, -] - [[package]] name = "mkdocs-include-markdown-plugin" -version = "7.2.2" +version = "7.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mkdocs" }, { name = "wcmatch" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ad/2d/bdf1aee3f4f7b34148b0f62298b62f03415160cb2707f09503c99a0a7cd5/mkdocs_include_markdown_plugin-7.2.2.tar.gz", hash = "sha256:f052ccb741eccf498116b826c1d78a2d761c56747372594709441cee0963fbc9", size = 25415, upload-time = "2026-03-29T15:15:14.2Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/2b/c788fc5c39ccba342eb9067be637327faebbc0e47da41ea79dc2526e693a/mkdocs_include_markdown_plugin-7.3.0.tar.gz", hash = "sha256:2800126746452e31c2e321bbd43c8190b356e0de353e20cbc16a34a3c3d6796c", size = 25527, upload-time = "2026-05-15T18:16:13.946Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/a5/f6b2f0aa805dbda52f6265e9aff1450c8643195442facf29d475bdeba15d/mkdocs_include_markdown_plugin-7.2.2-py3-none-any.whl", hash = "sha256:f2ec4487cf32d3e33ca528f9366f20fb9280ded9c8d1630eb2bbda244962dcd1", size = 29528, upload-time = "2026-03-29T15:15:13.079Z" }, + { url = "https://files.pythonhosted.org/packages/85/84/c0109c5991b89cbf028b8597f73fa7bd6a6b092407be2369a354b52737ab/mkdocs_include_markdown_plugin-7.3.0-py3-none-any.whl", hash = "sha256:5b5c99b5d3c9b9ce0114a9e60353bbafb6be53a26c2d3b74ec6b767a7a8e55ca", size = 29675, upload-time = "2026-05-15T18:16:12.654Z" }, ] [[package]] @@ -750,7 +685,7 @@ wheels = [ [[package]] name = "mkdocstrings" -version = "1.0.4" +version = "1.0.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jinja2" }, @@ -760,102 +695,166 @@ dependencies = [ { name = "mkdocs-autorefs" }, { name = "pymdown-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1d/5d/f888d4d3eb31359b327bc9b17a212d6ef03fe0b0682fbb3fc2cb849fb12b/mkdocstrings-1.0.4.tar.gz", hash = "sha256:3969a6515b77db65fd097b53c1b7aa4ae840bd71a2ee62a6a3e89503446d7172", size = 100088, upload-time = "2026-04-15T09:16:53.376Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/71/f85bdf13355073ae15a7375f09879375a830553552e58c1c4b7e0bbc5c8b/mkdocstrings-1.0.6.tar.gz", hash = "sha256:a0b8c2bdd29a6416c80d717aa369bbf7831946bd9f23c2a66db1b1dbe7693dbd", size = 100649, upload-time = "2026-07-11T19:38:05.732Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl", hash = "sha256:63464b4b29053514f32a1dbbf604e52876d5e638111b0c295ab7ed3cac73ca9b", size = 35560, upload-time = "2026-04-15T09:16:51.436Z" }, + { url = "https://files.pythonhosted.org/packages/5d/5b/4c1902e8bdd5c4db63284e9d101dece4038d4025d6d88850ffe0a1578980/mkdocstrings-1.0.6-py3-none-any.whl", hash = "sha256:2703708697487d1b6d6d7b412e176fa436edf120c1bf81dc9e126b12d00893c7", size = 35787, upload-time = "2026-07-11T19:38:04.417Z" }, ] [[package]] name = "mkdocstrings-python" -version = "2.0.3" +version = "2.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "griffelib" }, { name = "mkdocs-autorefs" }, { name = "mkdocstrings" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/29/33/c225eaf898634bdda489a6766fc35d1683c640bffe0e0acd10646b13536d/mkdocstrings_python-2.0.3.tar.gz", hash = "sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8", size = 199083, upload-time = "2026-02-20T10:38:36.368Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/b6/e858701499d57eee8b3fd8e78168083956c6683ddbe727b46758b19e1119/mkdocstrings_python-2.0.5.tar.gz", hash = "sha256:3a4d92556ad39637e88af94a5374213af9a8e3040c3824ceaed04b486c017594", size = 199578, upload-time = "2026-06-19T10:41:08.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/fc/10ab7e80650a9c9e8f4f1105f8c8e73567f88ed0c06ada589ab81d38687c/mkdocstrings_python-2.0.5-py3-none-any.whl", hash = "sha256:30c837bbff016549f659fcba6539ac351303f0fd7e713c89a040611072236e9d", size = 104951, upload-time = "2026-06-19T10:41:07.378Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.12' and sys_platform == 'emscripten'", + "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl", hash = "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12", size = 104779, upload-time = "2026-02-20T10:38:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" }, + { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" }, + { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" }, + { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" }, + { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" }, + { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, + { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" }, + { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, + { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, + { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" }, + { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, + { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, + { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, + { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, + { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" }, + { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" }, + { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" }, + { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" }, + { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" }, + { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" }, + { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" }, + { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" }, + { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, + { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" }, + { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" }, ] [[package]] name = "numpy" -version = "2.4.4" +version = "2.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/22/fd/89965aa4ac08c74998539fcbf24fa3540f3e15237fbeb6bcf9c908f4aade/numpy-2.5.1.tar.gz", hash = "sha256:a48a113e6afea91f5608793bafa7ef2ad481fefbda87ec5069f483de61cb9fa3", size = 20755553, upload-time = "2026-07-04T17:08:00.933Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/c6/4218570d8c8ecc9704b5157a3348e486e84ef4be0ed3e38218ab473c83d2/numpy-2.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db", size = 16976799, upload-time = "2026-03-29T13:18:15.438Z" }, - { url = "https://files.pythonhosted.org/packages/dd/92/b4d922c4a5f5dab9ed44e6153908a5c665b71acf183a83b93b690996e39b/numpy-2.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0", size = 14971552, upload-time = "2026-03-29T13:18:18.606Z" }, - { url = "https://files.pythonhosted.org/packages/8a/dc/df98c095978fa6ee7b9a9387d1d58cbb3d232d0e69ad169a4ce784bde4fd/numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015", size = 5476566, upload-time = "2026-03-29T13:18:21.532Z" }, - { url = "https://files.pythonhosted.org/packages/28/34/b3fdcec6e725409223dd27356bdf5a3c2cc2282e428218ecc9cb7acc9763/numpy-2.4.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40", size = 6806482, upload-time = "2026-03-29T13:18:23.634Z" }, - { url = "https://files.pythonhosted.org/packages/68/62/63417c13aa35d57bee1337c67446761dc25ea6543130cf868eace6e8157b/numpy-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d", size = 15973376, upload-time = "2026-03-29T13:18:26.677Z" }, - { url = "https://files.pythonhosted.org/packages/cf/c5/9fcb7e0e69cef59cf10c746b84f7d58b08bc66a6b7d459783c5a4f6101a6/numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502", size = 16925137, upload-time = "2026-03-29T13:18:30.14Z" }, - { url = "https://files.pythonhosted.org/packages/7e/43/80020edacb3f84b9efdd1591120a4296462c23fd8db0dde1666f6ef66f13/numpy-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd", size = 17329414, upload-time = "2026-03-29T13:18:33.733Z" }, - { url = "https://files.pythonhosted.org/packages/fd/06/af0658593b18a5f73532d377188b964f239eb0894e664a6c12f484472f97/numpy-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5", size = 18658397, upload-time = "2026-03-29T13:18:37.511Z" }, - { url = "https://files.pythonhosted.org/packages/e6/ce/13a09ed65f5d0ce5c7dd0669250374c6e379910f97af2c08c57b0608eee4/numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e", size = 6239499, upload-time = "2026-03-29T13:18:40.372Z" }, - { url = "https://files.pythonhosted.org/packages/bd/63/05d193dbb4b5eec1eca73822d80da98b511f8328ad4ae3ca4caf0f4db91d/numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e", size = 12614257, upload-time = "2026-03-29T13:18:42.95Z" }, - { url = "https://files.pythonhosted.org/packages/87/c5/8168052f080c26fa984c413305012be54741c9d0d74abd7fbeeccae3889f/numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e", size = 10486775, upload-time = "2026-03-29T13:18:45.835Z" }, - { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" }, - { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, - { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, - { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" }, - { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, - { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, - { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, - { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, - { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, - { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, - { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, - { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, - { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, - { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, - { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" }, - { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, - { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, - { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, - { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, - { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, - { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, - { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, - { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, - { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, - { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" }, - { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, - { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, - { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, - { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, - { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, - { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, - { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, - { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, - { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, - { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, - { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, - { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, - { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, - { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, - { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, - { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, - { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, - { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, - { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, - { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, - { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, - { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, - { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, - { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, - { url = "https://files.pythonhosted.org/packages/6b/33/8fae8f964a4f63ed528264ddf25d2b683d0b663e3cba26961eb838a7c1bd/numpy-2.4.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4", size = 16854491, upload-time = "2026-03-29T13:21:38.03Z" }, - { url = "https://files.pythonhosted.org/packages/bc/d0/1aabee441380b981cf8cdda3ae7a46aa827d1b5a8cce84d14598bc94d6d9/numpy-2.4.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e", size = 14895830, upload-time = "2026-03-29T13:21:41.509Z" }, - { url = "https://files.pythonhosted.org/packages/a5/b8/aafb0d1065416894fccf4df6b49ef22b8db045187949545bced89c034b8e/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c", size = 5400927, upload-time = "2026-03-29T13:21:44.747Z" }, - { url = "https://files.pythonhosted.org/packages/d6/77/063baa20b08b431038c7f9ff5435540c7b7265c78cf56012a483019ca72d/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3", size = 6715557, upload-time = "2026-03-29T13:21:47.406Z" }, - { url = "https://files.pythonhosted.org/packages/c7/a8/379542d45a14f149444c5c4c4e7714707239ce9cc1de8c2803958889da14/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7", size = 15804253, upload-time = "2026-03-29T13:21:50.753Z" }, - { url = "https://files.pythonhosted.org/packages/a2/c8/f0a45426d6d21e7ea3310a15cf90c43a14d9232c31a837702dba437f3373/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f", size = 16753552, upload-time = "2026-03-29T13:21:54.344Z" }, - { url = "https://files.pythonhosted.org/packages/04/74/f4c001f4714c3ad9ce037e18cf2b9c64871a84951eaa0baf683a9ca9301c/numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119", size = 12509075, upload-time = "2026-03-29T13:21:57.644Z" }, + { url = "https://files.pythonhosted.org/packages/62/7b/14687aa674250e5e546f616f486b0d56d3631cd5b2415739141ce40bdcea/numpy-2.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c889b56fe48b1018f764b0eec8df59ab654e9148aa91faa12596043500de277", size = 16801574, upload-time = "2026-07-04T17:06:12.423Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/cc5bb2a3f2913d27d6dbb2c78d25921fabaedc6741d4a5a615a11f3c5bf3/numpy-2.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab451b59c5643c570974c43aef780703ef1d3b4965d2be07afd530615a9358d1", size = 11772250, upload-time = "2026-07-04T17:06:15.726Z" }, + { url = "https://files.pythonhosted.org/packages/42/77/fdf34a71dd30f54979b18603bee915e0aaf825b07afe79acd60b04b691e2/numpy-2.5.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:78798bd5b9ad744056af8efa90e3b9ddaa53272a0848a483084a1cc0a13b2dc0", size = 5331516, upload-time = "2026-07-04T17:06:17.913Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e2/eb7efa015b4cce41e2517bf182a7fce0d7d5b9d9ed76a29bfa0f4fe4505c/numpy-2.5.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:2ae0ca40bcb22d6ba59c1dfd5446f49940b0f2d821fde133f10dda11f816b84e", size = 6664863, upload-time = "2026-07-04T17:06:20.02Z" }, + { url = "https://files.pythonhosted.org/packages/a9/4b/a2b32dd94ee9ffbeecb28152240042a3949db33b1c834d44090b80e1b3b8/numpy-2.5.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61ac47e772e6b8ea489e1d2f441a34c5c3ac17327e7ce294cbdf535795ad4e75", size = 15167977, upload-time = "2026-07-04T17:06:21.621Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a9/6e73d68500f80773f65f0654ea932019d6694329a0eb0ed0533de38df376/numpy-2.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:59fda5e192b570217ec2580c96f00e9a7e12ef6866a900eb089b62c1a32545ca", size = 16672469, upload-time = "2026-07-04T17:06:24.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/7d/ad3e59015135f5261c95fd4cafeff159c955febd83a99a1d9250c4233815/numpy-2.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f7119ebff1a9829e9f431a4f9d28e703023bb6b9fe7c8f724467dbfc27c94ab3", size = 16527531, upload-time = "2026-07-04T17:06:26.69Z" }, + { url = "https://files.pythonhosted.org/packages/83/d0/a39b2fbcde9cb17a1dac678f254b33a6336298af9df338824c685425d5e8/numpy-2.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e824c2acf8862052246be5a44c15da1777940c60d010dd2aab897824d9c430f9", size = 18431940, upload-time = "2026-07-04T17:06:29.521Z" }, + { url = "https://files.pythonhosted.org/packages/04/12/cff070947791c1ed425ff76413189adbdc2fbe215eba7ce7fa454a03c7f8/numpy-2.5.1-cp312-cp312-win32.whl", hash = "sha256:08d60c810432eb83360958dea0999ac4cfb94531ea8efcbf0b7f277c2068aeb2", size = 6066764, upload-time = "2026-07-04T17:06:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/65/66/53f31807a48a750f9d748da273bc3fcedd12b27ff1f3e373bfec55ef2dc0/numpy-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:f7d60026c0bdb1380e83bfa7a0419c4577ee4b9a08880afcb6dadeb74c649fa2", size = 12430966, upload-time = "2026-07-04T17:06:34.926Z" }, + { url = "https://files.pythonhosted.org/packages/2b/2a/d1a88066b1c14186f5d3c0d18c94f17b064511982bab0578d49ee9d43c29/numpy-2.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:17a25e09640602e10bc8de0e6fa2b3fd68eedd84ba6d7842dc8f32f9ab87bd0b", size = 10350488, upload-time = "2026-07-04T17:06:37.785Z" }, + { url = "https://files.pythonhosted.org/packages/eb/07/ec2a3f0c91761581d4b7104a740791800025983f9a4dc4e73f91a99aeac4/numpy-2.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0bfebd8695f9863592fe744be833a258120b14a9f39da255e8aa8fade2c0ddd1", size = 16796419, upload-time = "2026-07-04T17:06:40.37Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ab/ddb499fc4f8780354395face5b65c7fd107bcd6e1d667a5f07d046956f6f/numpy-2.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30b44a6b53a7ae63c54c089a8726e5563ed302716c5b7ccc85afade40b0e7ff6", size = 11765832, upload-time = "2026-07-04T17:06:42.768Z" }, + { url = "https://files.pythonhosted.org/packages/88/b3/3c28c558a09fc72100c646dac6d2fce8e834c471b0edca01a29996706117/numpy-2.5.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6165343f81b56ef8f514f396989e529b61d9dc709b99421b07e9f3e698e2287d", size = 5325143, upload-time = "2026-07-04T17:06:45.466Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0e/ce19b985bb15c596f4f05954e76cccc77c845083b3b8f938a6c68e523128/numpy-2.5.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4939237038ada79308dda3204ac6462df056b5672b2e25db1149cf873668b3e1", size = 6659749, upload-time = "2026-07-04T17:06:47.288Z" }, + { url = "https://files.pythonhosted.org/packages/2e/20/1ee6614d64332a1bba6411f38e68cb79eec1b2459e20a623777c5c5492a2/numpy-2.5.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c6759f538fb912fc46de0a6b1758ccf7b57bc7c7ebebc23974fdac3de8db0cd", size = 15164716, upload-time = "2026-07-04T17:06:49.494Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a7/2bcd3fdbb87804755c35b729bf8709d62025c5f4cfd7d5b2415997097515/numpy-2.5.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9726558e8db4a5bf7929a70ae50f63abda4daf0efe810e3bfbab95976f75fc1a", size = 16661440, upload-time = "2026-07-04T17:06:52.061Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d7/a41e3310c886fe457d36e670bbf24fae411aca8a7b6ad92a32afd924077c/numpy-2.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3935f3b419b244a02732676fa5317a9193cc596a4c0646db07e5b421229ac9f7", size = 16526305, upload-time = "2026-07-04T17:06:54.605Z" }, + { url = "https://files.pythonhosted.org/packages/53/75/4333a9a707c1edd3a4e1a0c58eca52c0f31e55089fa80db02b5565b24df7/numpy-2.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc932a65ded7ce9013d120845a2514dcccb1a67bfc8deb8d37633762951904a6", size = 18423008, upload-time = "2026-07-04T17:06:57.54Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/e314a32b1c11a2ffe818ddad3a57b50b4b6e1b6c487192eb50cdef0415d0/numpy-2.5.1-cp313-cp313-win32.whl", hash = "sha256:4b4ff1608417eb7a59da7b967bbb798cacfe071d2caf526a24281cd562072ed9", size = 6063885, upload-time = "2026-07-04T17:07:00.14Z" }, + { url = "https://files.pythonhosted.org/packages/10/70/800b3fca480af32df9e8ea9f3d4a0c8feb4b32d7f195d174eabbda4829ad/numpy-2.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:6c3fe51bc6a16453d452997053454f309e8e0ed7b42d6b361ce4ac8c32913d74", size = 12425674, upload-time = "2026-07-04T17:07:02.387Z" }, + { url = "https://files.pythonhosted.org/packages/8b/0b/196350c122f50f6ca56846f2d71efd5e0d24b7b2e07355e019b2e2c7a11e/numpy-2.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f7feb014281029e628ba2d5a007407443b06e418b6fe451d1e2adcbc8eba0107", size = 10350256, upload-time = "2026-07-04T17:07:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/db/f4/731b6085a83faf6ca843394cbd5e217280c214399f7e8b21b9f552af0ae2/numpy-2.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7c786fe9a5bbe360022e584c5a34cf6b54265c71bd7ec8ac3d8fec38968071f8", size = 16795063, upload-time = "2026-07-04T17:07:07.374Z" }, + { url = "https://files.pythonhosted.org/packages/bf/64/0e215f2048dd11a55bb989ed41b3585ef57452404e638d703a211a3e4157/numpy-2.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32985c896d897419ef8da6917872d80b78ad0ea26d85b23245c7366ffde76d75", size = 11776652, upload-time = "2026-07-04T17:07:09.907Z" }, + { url = "https://files.pythonhosted.org/packages/b5/59/2b844c7a6e9deff69b404a66221e1542937734f65d5e6e39411876053862/numpy-2.5.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:efd736408cc97c79b9e6917338dfc8f06013b2274f992e96b1d9a81a71e2a2c2", size = 5335944, upload-time = "2026-07-04T17:07:12.227Z" }, + { url = "https://files.pythonhosted.org/packages/86/51/9bf7cb2cabcebc9e017e4ec7e6322b378317a542c08b4cb68479c1efc716/numpy-2.5.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:ab84dc6b074fa881cae55bea94cc4f68e285181ba7f32497bf7dee6b1496165b", size = 6656266, upload-time = "2026-07-04T17:07:14.368Z" }, + { url = "https://files.pythonhosted.org/packages/83/3e/fb7615b211b82a32f44d5180a6d421b61f84d4fadd578b48ba4ac34e189f/numpy-2.5.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caf3e317d33d60c37986b452613f4ab51246d0691350c03d0cb4a898627f4a95", size = 15179720, upload-time = "2026-07-04T17:07:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/0f992cb24560673496c5d68de61913b57166ce530ffda07c1f280e0cc464/numpy-2.5.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54ad769f17bc2d833b620851989f62054fb9ab93c969d9e1dc3c8e3d56beea21", size = 16664835, upload-time = "2026-07-04T17:07:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/97d6475ee91afe2587797d09446f9d3e475ad4cb681662d824809327b75a/numpy-2.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c12afb53450fa976d4c681c50a7423729a4c51c0465ed9f32b8a9cabbc472373", size = 16539135, upload-time = "2026-07-04T17:07:22.015Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/4db81e4ba0be7e2776b1de68c82aa862c7f8ec27e1b4927d4ae075e20678/numpy-2.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c11c405efc5ff6816d5983c96cdfa215bab3428961243af3ff59b228490438", size = 18426684, upload-time = "2026-07-04T17:07:24.941Z" }, + { url = "https://files.pythonhosted.org/packages/1f/64/c0ba2d90724d450279a7df8f32057241070250a26a7e2b5337d77347f481/numpy-2.5.1-cp314-cp314-win32.whl", hash = "sha256:f2479a47f8d5932d1718168a681ad6e536a9df484c83cfcf9de365e164537ace", size = 6116103, upload-time = "2026-07-04T17:07:27.622Z" }, + { url = "https://files.pythonhosted.org/packages/c1/1a/837f9ed7405adcd7a40538792eb169eddd8fa5630c16a1ef49dae71a30f4/numpy-2.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:24d0eb82c0541d3415a33425db64ae439dffccd7b4dbcb30e7c35120205c506a", size = 12562177, upload-time = "2026-07-04T17:07:29.887Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/49707938b6dd0a78a9178dd93227dc89e4c11af47f5c798d70366e8d0483/numpy-2.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:5a4c988b38d261deeeaad9954e3deb091ad905c94e8bb6708654ef1d97f286b0", size = 10627739, upload-time = "2026-07-04T17:07:32.568Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c7/bb4b882cfe7f299cbc8b66e42e7dd78cf9d14e40f9469fc5e3db7e15b3bd/numpy-2.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a33276be12fa045805f477f22482088b66bb758ffbe89a9d21457de863a32e22", size = 11894709, upload-time = "2026-07-04T17:07:34.941Z" }, + { url = "https://files.pythonhosted.org/packages/40/3f/5af7f4a7f6224aef48017aa82bb6174c7a659d724be0c75017b7e64a55b4/numpy-2.5.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f089d7b00756190aacf1f5d34bdf38c3c430ac82b4f868f8cede73380460fce7", size = 5453810, upload-time = "2026-07-04T17:07:37.495Z" }, + { url = "https://files.pythonhosted.org/packages/20/c9/3474309bc94d634d3f9c3eddf03250ecb8c22cd948ef16fef69a77cc5d7b/numpy-2.5.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:09e9bfd8d2cf479c7d174804fb3811c53a8e9f20a37444008606b57d6b7a826d", size = 6761189, upload-time = "2026-07-04T17:07:39.563Z" }, + { url = "https://files.pythonhosted.org/packages/90/8a/558ae39fdd55d7e7f7fef9a84a6e964ac6b23edbd2a07e52bb084500507d/numpy-2.5.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e68d8dd1e7eba712948f2053a29ec86917bc70ba1358df869d9f06649ef9cf09", size = 15225039, upload-time = "2026-07-04T17:07:41.682Z" }, + { url = "https://files.pythonhosted.org/packages/63/27/ca7392b2d030277bdf0273e7d23255b3ee57d57a7c170a6f4fb3981e1e5d/numpy-2.5.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99d5095fa265a0c4152e7bb12759e14381ef5496152f1ce58f44bdf55c44beb4", size = 16701306, upload-time = "2026-07-04T17:07:44.611Z" }, + { url = "https://files.pythonhosted.org/packages/02/42/03d53ae7996c44d4374a8262e9dc41671fd56cbb98f7d47ef85cf5da4c6b/numpy-2.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ab87a91b3cc3382b8956095bd8f95e00cf679bb81554339be1a2ba404a1473c1", size = 16589955, upload-time = "2026-07-04T17:07:47.694Z" }, + { url = "https://files.pythonhosted.org/packages/7b/15/6c1784ae469640e65db111e9a34b3d0f14d91e8a38b9ce34810ced370dbb/numpy-2.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:224ca51130ef7da85bea2191625181cb4f337f9cb64b471f10c1a12aa8b60077", size = 18464252, upload-time = "2026-07-04T17:07:50.684Z" }, + { url = "https://files.pythonhosted.org/packages/94/a8/f98e50356cf167df656c526c2dfeec2d7dde182f2a3da4b458a5938e2776/numpy-2.5.1-cp314-cp314t-win32.whl", hash = "sha256:6eab239876581b2b3c5a242281b6007bbdbcd1c7085d7709bb57c5929b11e6bf", size = 6263298, upload-time = "2026-07-04T17:07:53.445Z" }, + { url = "https://files.pythonhosted.org/packages/72/ac/96ae880cdecad0b3275d9359fcec72667b49a4863c9f12942e43679dda02/numpy-2.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:83ce9c80d5b521b0d77ddcbe5447c218d247929b6cc056ca5351342accfff0af", size = 12748623, upload-time = "2026-07-04T17:07:55.384Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5a/4d2b1601df3602dba7a14f3348ba9bfe94a18adb428e693df6154c293831/numpy-2.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:5a6db61f9aaa57e369905c67d852045d3c4f7126405b29d09b19dec118e9c9cb", size = 10697674, upload-time = "2026-07-04T17:07:58.506Z" }, ] [[package]] @@ -878,62 +877,63 @@ wheels = [ [[package]] name = "pandas" -version = "3.0.2" +version = "3.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "python-dateutil" }, { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/99/b342345300f13440fe9fe385c3c481e2d9a595ee3bab4d3219247ac94e9a/pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043", size = 4645855, upload-time = "2026-03-31T06:48:30.816Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/35/6411db530c618e0e0005187e35aa02ce60ae4c4c4d206964a2f978217c27/pandas-3.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a727a73cbdba2f7458dc82449e2315899d5140b449015d822f515749a46cbbe0", size = 10326926, upload-time = "2026-03-31T06:46:08.29Z" }, - { url = "https://files.pythonhosted.org/packages/c4/d3/b7da1d5d7dbdc5ef52ed7debd2b484313b832982266905315dad5a0bf0b1/pandas-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dbbd4aa20ca51e63b53bbde6a0fa4254b1aaabb74d2f542df7a7959feb1d760c", size = 9926987, upload-time = "2026-03-31T06:46:11.724Z" }, - { url = "https://files.pythonhosted.org/packages/52/77/9b1c2d6070b5dbe239a7bc889e21bfa58720793fb902d1e070695d87c6d0/pandas-3.0.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:339dda302bd8369dedeae979cb750e484d549b563c3f54f3922cb8ff4978c5eb", size = 10757067, upload-time = "2026-03-31T06:46:14.903Z" }, - { url = "https://files.pythonhosted.org/packages/20/17/ec40d981705654853726e7ac9aea9ddbb4a5d9cf54d8472222f4f3de06c2/pandas-3.0.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61c2fd96d72b983a9891b2598f286befd4ad262161a609c92dc1652544b46b76", size = 11258787, upload-time = "2026-03-31T06:46:17.683Z" }, - { url = "https://files.pythonhosted.org/packages/90/e3/3f1126d43d3702ca8773871a81c9f15122a1f412342cc56284ffda5b1f70/pandas-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c934008c733b8bbea273ea308b73b3156f0181e5b72960790b09c18a2794fe1e", size = 11771616, upload-time = "2026-03-31T06:46:20.532Z" }, - { url = "https://files.pythonhosted.org/packages/2e/cf/0f4e268e1f5062e44a6bda9f925806721cd4c95c2b808a4c82ebe914f96b/pandas-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:60a80bb4feacbef5e1447a3f82c33209c8b7e07f28d805cfd1fb951e5cb443aa", size = 12337623, upload-time = "2026-03-31T06:46:23.754Z" }, - { url = "https://files.pythonhosted.org/packages/44/a0/97a6339859d4acb2536efb24feb6708e82f7d33b2ed7e036f2983fcced82/pandas-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:ed72cb3f45190874eb579c64fa92d9df74e98fd63e2be7f62bce5ace0ade61df", size = 9897372, upload-time = "2026-03-31T06:46:26.703Z" }, - { url = "https://files.pythonhosted.org/packages/8f/eb/781516b808a99ddf288143cec46b342b3016c3414d137da1fdc3290d8860/pandas-3.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:f12b1a9e332c01e09510586f8ca9b108fd631fd656af82e452d7315ef6df5f9f", size = 9154922, upload-time = "2026-03-31T06:46:30.284Z" }, - { url = "https://files.pythonhosted.org/packages/f3/b0/c20bd4d6d3f736e6bd6b55794e9cd0a617b858eaad27c8f410ea05d953b7/pandas-3.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:232a70ebb568c0c4d2db4584f338c1577d81e3af63292208d615907b698a0f18", size = 10347921, upload-time = "2026-03-31T06:46:33.36Z" }, - { url = "https://files.pythonhosted.org/packages/35/d0/4831af68ce30cc2d03c697bea8450e3225a835ef497d0d70f31b8cdde965/pandas-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:970762605cff1ca0d3f71ed4f3a769ea8f85fc8e6348f6e110b8fea7e6eb5a14", size = 9888127, upload-time = "2026-03-31T06:46:36.253Z" }, - { url = "https://files.pythonhosted.org/packages/61/a9/16ea9346e1fc4a96e2896242d9bc674764fb9049b0044c0132502f7a771e/pandas-3.0.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aff4e6f4d722e0652707d7bcb190c445fe58428500c6d16005b02401764b1b3d", size = 10399577, upload-time = "2026-03-31T06:46:39.224Z" }, - { url = "https://files.pythonhosted.org/packages/c4/a8/3a61a721472959ab0ce865ef05d10b0d6bfe27ce8801c99f33d4fa996e65/pandas-3.0.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef8b27695c3d3dc78403c9a7d5e59a62d5464a7e1123b4e0042763f7104dc74f", size = 10880030, upload-time = "2026-03-31T06:46:42.412Z" }, - { url = "https://files.pythonhosted.org/packages/da/65/7225c0ea4d6ce9cb2160a7fb7f39804871049f016e74782e5dade4d14109/pandas-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f8d68083e49e16b84734eb1a4dcae4259a75c90fb6e2251ab9a00b61120c06ab", size = 11409468, upload-time = "2026-03-31T06:46:45.2Z" }, - { url = "https://files.pythonhosted.org/packages/fa/5b/46e7c76032639f2132359b5cf4c785dd8cf9aea5ea64699eac752f02b9db/pandas-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:32cc41f310ebd4a296d93515fcac312216adfedb1894e879303987b8f1e2b97d", size = 11936381, upload-time = "2026-03-31T06:46:48.293Z" }, - { url = "https://files.pythonhosted.org/packages/7b/8b/721a9cff6fa6a91b162eb51019c6243b82b3226c71bb6c8ef4a9bd65cbc6/pandas-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:a4785e1d6547d8427c5208b748ae2efb64659a21bd82bf440d4262d02bfa02a4", size = 9744993, upload-time = "2026-03-31T06:46:51.488Z" }, - { url = "https://files.pythonhosted.org/packages/d5/18/7f0bd34ae27b28159aa80f2a6799f47fda34f7fb938a76e20c7b7fe3b200/pandas-3.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:08504503f7101300107ecdc8df73658e4347586db5cfdadabc1592e9d7e7a0fd", size = 9056118, upload-time = "2026-03-31T06:46:54.548Z" }, - { url = "https://files.pythonhosted.org/packages/bf/ca/3e639a1ea6fcd0617ca4e8ca45f62a74de33a56ae6cd552735470b22c8d3/pandas-3.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5918ba197c951dec132b0c5929a00c0bf05d5942f590d3c10a807f6e15a57d3", size = 10321105, upload-time = "2026-03-31T06:46:57.327Z" }, - { url = "https://files.pythonhosted.org/packages/0b/77/dbc82ff2fb0e63c6564356682bf201edff0ba16c98630d21a1fb312a8182/pandas-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d606a041c89c0a474a4702d532ab7e73a14fe35c8d427b972a625c8e46373668", size = 9864088, upload-time = "2026-03-31T06:46:59.935Z" }, - { url = "https://files.pythonhosted.org/packages/5c/2b/341f1b04bbca2e17e13cd3f08c215b70ef2c60c5356ef1e8c6857449edc7/pandas-3.0.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:710246ba0616e86891b58ab95f2495143bb2bc83ab6b06747c74216f583a6ac9", size = 10369066, upload-time = "2026-03-31T06:47:02.792Z" }, - { url = "https://files.pythonhosted.org/packages/12/c5/cbb1ffefb20a93d3f0e1fdcda699fb84976210d411b008f97f48bf6ce27e/pandas-3.0.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d3cfe227c725b1f3dff4278b43d8c784656a42a9325b63af6b1492a8232209e", size = 10876780, upload-time = "2026-03-31T06:47:06.205Z" }, - { url = "https://files.pythonhosted.org/packages/98/fe/2249ae5e0a69bd0ddf17353d0a5d26611d70970111f5b3600cdc8be883e7/pandas-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c3b723df9087a9a9a840e263ebd9f88b64a12075d1bf2ea401a5a42f254f084d", size = 11375181, upload-time = "2026-03-31T06:47:09.383Z" }, - { url = "https://files.pythonhosted.org/packages/de/64/77a38b09e70b6464883b8d7584ab543e748e42c1b5d337a2ee088e0df741/pandas-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3096110bf9eac0070b7208465f2740e2d8a670d5cb6530b5bb884eca495fd39", size = 11928899, upload-time = "2026-03-31T06:47:12.686Z" }, - { url = "https://files.pythonhosted.org/packages/5e/52/42855bf626868413f761addd574acc6195880ae247a5346477a4361c3acb/pandas-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:07a10f5c36512eead51bc578eb3354ad17578b22c013d89a796ab5eee90cd991", size = 9746574, upload-time = "2026-03-31T06:47:15.64Z" }, - { url = "https://files.pythonhosted.org/packages/88/39/21304ae06a25e8bf9fc820d69b29b2c495b2ae580d1e143146c309941760/pandas-3.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:5fdbfa05931071aba28b408e59226186b01eb5e92bea2ab78b65863ca3228d84", size = 9047156, upload-time = "2026-03-31T06:47:18.595Z" }, - { url = "https://files.pythonhosted.org/packages/72/20/7defa8b27d4f330a903bb68eea33be07d839c5ea6bdda54174efcec0e1d2/pandas-3.0.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:dbc20dea3b9e27d0e66d74c42b2d0c1bed9c2ffe92adea33633e3bedeb5ac235", size = 10756238, upload-time = "2026-03-31T06:47:22.012Z" }, - { url = "https://files.pythonhosted.org/packages/e9/95/49433c14862c636afc0e9b2db83ff16b3ad92959364e52b2955e44c8e94c/pandas-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b75c347eff42497452116ce05ef461822d97ce5b9ff8df6edacb8076092c855d", size = 10408520, upload-time = "2026-03-31T06:47:25.197Z" }, - { url = "https://files.pythonhosted.org/packages/3b/f8/462ad2b5881d6b8ec8e5f7ed2ea1893faa02290d13870a1600fe72ad8efc/pandas-3.0.2-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1478075142e83a5571782ad007fb201ed074bdeac7ebcc8890c71442e96adf7", size = 10324154, upload-time = "2026-03-31T06:47:28.097Z" }, - { url = "https://files.pythonhosted.org/packages/0a/65/d1e69b649cbcddda23ad6e4c40ef935340f6f652a006e5cbc3555ac8adb3/pandas-3.0.2-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5880314e69e763d4c8b27937090de570f1fb8d027059a7ada3f7f8e98bdcb677", size = 10714449, upload-time = "2026-03-31T06:47:30.85Z" }, - { url = "https://files.pythonhosted.org/packages/47/a4/85b59bc65b8190ea3689882db6cdf32a5003c0ccd5a586c30fdcc3ffc4fc/pandas-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b5329e26898896f06035241a626d7c335daa479b9bbc82be7c2742d048e41172", size = 11338475, upload-time = "2026-03-31T06:47:34.026Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c4/bc6966c6e38e5d9478b935272d124d80a589511ed1612a5d21d36f664c68/pandas-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:81526c4afd31971f8b62671442a4b2b51e0aa9acc3819c9f0f12a28b6fcf85f1", size = 11786568, upload-time = "2026-03-31T06:47:36.941Z" }, - { url = "https://files.pythonhosted.org/packages/e8/74/09298ca9740beed1d3504e073d67e128aa07e5ca5ca2824b0c674c0b8676/pandas-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:7cadd7e9a44ec13b621aec60f9150e744cfc7a3dd32924a7e2f45edff31823b0", size = 10488652, upload-time = "2026-03-31T06:47:40.612Z" }, - { url = "https://files.pythonhosted.org/packages/bb/40/c6ea527147c73b24fc15c891c3fcffe9c019793119c5742b8784a062c7db/pandas-3.0.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:db0dbfd2a6cdf3770aa60464d50333d8f3d9165b2f2671bcc299b72de5a6677b", size = 10326084, upload-time = "2026-03-31T06:47:43.834Z" }, - { url = "https://files.pythonhosted.org/packages/95/25/bdb9326c3b5455f8d4d3549fce7abcf967259de146fe2cf7a82368141948/pandas-3.0.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0555c5882688a39317179ab4a0ed41d3ebc8812ab14c69364bbee8fb7a3f6288", size = 9914146, upload-time = "2026-03-31T06:47:46.67Z" }, - { url = "https://files.pythonhosted.org/packages/8d/77/3a227ff3337aa376c60d288e1d61c5d097131d0ac71f954d90a8f369e422/pandas-3.0.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01f31a546acd5574ef77fe199bc90b55527c225c20ccda6601cf6b0fd5ed597c", size = 10444081, upload-time = "2026-03-31T06:47:49.681Z" }, - { url = "https://files.pythonhosted.org/packages/15/88/3cdd54fa279341afa10acf8d2b503556b1375245dccc9315659f795dd2e9/pandas-3.0.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:deeca1b5a931fdf0c2212c8a659ade6d3b1edc21f0914ce71ef24456ca7a6535", size = 10897535, upload-time = "2026-03-31T06:47:53.033Z" }, - { url = "https://files.pythonhosted.org/packages/06/9d/98cc7a7624f7932e40f434299260e2917b090a579d75937cb8a57b9d2de3/pandas-3.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f48afd9bb13300ffb5a3316973324c787054ba6665cda0da3fbd67f451995db", size = 11446992, upload-time = "2026-03-31T06:47:56.193Z" }, - { url = "https://files.pythonhosted.org/packages/9a/cd/19ff605cc3760e80602e6826ddef2824d8e7050ed80f2e11c4b079741dc3/pandas-3.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6c4d8458b97a35717b62469a4ea0e85abd5ed8687277f5ccfc67f8a5126f8c53", size = 11968257, upload-time = "2026-03-31T06:47:59.137Z" }, - { url = "https://files.pythonhosted.org/packages/db/60/aba6a38de456e7341285102bede27514795c1eaa353bc0e7638b6b785356/pandas-3.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:b35d14bb5d8285d9494fe93815a9e9307c0876e10f1e8e89ac5b88f728ec8dcf", size = 9865893, upload-time = "2026-03-31T06:48:02.038Z" }, - { url = "https://files.pythonhosted.org/packages/08/71/e5ec979dd2e8a093dacb8864598c0ff59a0cee0bbcdc0bfec16a51684d4f/pandas-3.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:63d141b56ef686f7f0d714cfb8de4e320475b86bf4b620aa0b7da89af8cbdbbb", size = 9188644, upload-time = "2026-03-31T06:48:05.045Z" }, - { url = "https://files.pythonhosted.org/packages/f1/6c/7b45d85db19cae1eb524f2418ceaa9d85965dcf7b764ed151386b7c540f0/pandas-3.0.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:140f0cffb1fa2524e874dde5b477d9defe10780d8e9e220d259b2c0874c89d9d", size = 10776246, upload-time = "2026-03-31T06:48:07.789Z" }, - { url = "https://files.pythonhosted.org/packages/a8/3e/7b00648b086c106e81766f25322b48aa8dfa95b55e621dbdf2fdd413a117/pandas-3.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae37e833ff4fed0ba352f6bdd8b73ba3ab3256a85e54edfd1ab51ae40cca0af8", size = 10424801, upload-time = "2026-03-31T06:48:10.897Z" }, - { url = "https://files.pythonhosted.org/packages/da/6e/558dd09a71b53b4008e7fc8a98ec6d447e9bfb63cdaeea10e5eb9b2dabe8/pandas-3.0.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d888a5c678a419a5bb41a2a93818e8ed9fd3172246555c0b37b7cc27027effd", size = 10345643, upload-time = "2026-03-31T06:48:13.7Z" }, - { url = "https://files.pythonhosted.org/packages/be/e3/921c93b4d9a280409451dc8d07b062b503bbec0531d2627e73a756e99a82/pandas-3.0.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b444dc64c079e84df91baa8bf613d58405645461cabca929d9178f2cd392398d", size = 10743641, upload-time = "2026-03-31T06:48:16.659Z" }, - { url = "https://files.pythonhosted.org/packages/56/ca/fd17286f24fa3b4d067965d8d5d7e14fe557dd4f979a0b068ac0deaf8228/pandas-3.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4544c7a54920de8eeacaa1466a6b7268ecfbc9bc64ab4dbb89c6bbe94d5e0660", size = 11361993, upload-time = "2026-03-31T06:48:19.475Z" }, - { url = "https://files.pythonhosted.org/packages/e4/a5/2f6ed612056819de445a433ca1f2821ac3dab7f150d569a59e9cc105de1d/pandas-3.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:734be7551687c00fbd760dc0522ed974f82ad230d4a10f54bf51b80d44a08702", size = 11815274, upload-time = "2026-03-31T06:48:22.695Z" }, - { url = "https://files.pythonhosted.org/packages/00/2f/b622683e99ec3ce00b0854bac9e80868592c5b051733f2cf3a868e5fea26/pandas-3.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:57a07209bebcbcf768d2d13c9b78b852f9a15978dac41b9e6421a81ad4cdd276", size = 10888530, upload-time = "2026-03-31T06:48:25.806Z" }, - { url = "https://files.pythonhosted.org/packages/cb/2b/f8434233fab2bd66a02ec014febe4e5adced20e2693e0e90a07d118ed30e/pandas-3.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:5371b72c2d4d415d08765f32d689217a43227484e81b2305b52076e328f6f482", size = 9455341, upload-time = "2026-03-31T06:48:28.418Z" }, + { url = "https://files.pythonhosted.org/packages/42/16/b5c76b838fd9bf6ce84d3a53346b8874ec05c5f0040d75ef2c320100cd2a/pandas-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:455f6f8139d4282188f526868dbc3c828470e88a3d9d59a891bd46a455f21b98", size = 10338495, upload-time = "2026-05-11T18:52:11.558Z" }, + { url = "https://files.pythonhosted.org/packages/5a/b0/a4ffc4ae74d2d822200dcc46898987d8eb6032d1e2b219cae39da6f5cbcc/pandas-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4e15135e2ee5df1063313e2425ceef8ac0f4ae775893815b0923651b806a5639", size = 9938250, upload-time = "2026-05-11T18:52:17.005Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b2/3323601a52caee42c019e370090ca4544b241437240ca04f786cce82b0cf/pandas-3.0.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:05f1f1752b8533ea03f7f39a9c15b1a058d067bb48f4748948e7a8691e0510f2", size = 10770558, upload-time = "2026-05-11T18:52:19.865Z" }, + { url = "https://files.pythonhosted.org/packages/32/f1/bbecd2f867b97abebe0f9b53d750f862251b40337e061b36676ded3d920f/pandas-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a1e45c80cceb3b4a21bc5939d52e8cbd8d9b7305309219d59e9754d9ce09e27", size = 11274611, upload-time = "2026-05-11T18:52:22.622Z" }, + { url = "https://files.pythonhosted.org/packages/7f/4f/eafabf2d5fae5adf143b4d18d3706c5efdc368a7c4eb1ee8a3eddabbd0f6/pandas-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:14da8316da4d0c5a77618425996bfb1248ca87fc2c1486e6fde4652bd18b5824", size = 11784670, upload-time = "2026-05-11T18:52:25.4Z" }, + { url = "https://files.pythonhosted.org/packages/49/44/1eb20389301b57b19cc099a1c2f662501f72f08a65f912d05822613c1532/pandas-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a55066a0505dae0ba2b50a46637db34b46f9094c65c5d4800794ef6335010938", size = 12353708, upload-time = "2026-05-11T18:52:28.139Z" }, + { url = "https://files.pythonhosted.org/packages/eb/62/c321f13b5ba1819fc8dca456c7fce578da2dcfecff1abbf0eaddf8406c0f/pandas-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:6674ab18ad8c57802867264b00e15e7bb904700cdd9046e3b2fa1fce237439ea", size = 9907609, upload-time = "2026-05-11T18:52:30.982Z" }, + { url = "https://files.pythonhosted.org/packages/53/85/1b7f563ebc6357c27233a02a96b589bcce1fa9c6eb89fb4f0e56421d277e/pandas-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:5cc09a68b3120e0f54870dede8287a7bb1fa463907e4fcec1ea77cab6179bf7a", size = 9165596, upload-time = "2026-05-11T18:52:33.334Z" }, + { url = "https://files.pythonhosted.org/packages/24/f1/392f8c5bfc16f66a0d2d41561c01627c228fe7ed2a0d056ef11315042570/pandas-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fed2ff7fd9779120e388e285fc029bd5cf9490cdd2e4166a9ee22c0e49a9ab09", size = 10357846, upload-time = "2026-05-11T18:52:36.143Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3d/b16412745651e855f357e5e66930248688378853a6e2698a214e331fba1f/pandas-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b168fc218fd80a6cbdbdbc1a97ddc7889ed057d7eb45f50d866ceab5f39904c4", size = 9899550, upload-time = "2026-05-11T18:52:38.976Z" }, + { url = "https://files.pythonhosted.org/packages/31/a8/fa2535168fffcedf67f4f6de28d2dd903a747ca7c8ea6989451aaeb3a92f/pandas-3.0.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0383c72c75cdcca61a9e116e611143902dbfd08bff356829c2f6d1cf40a9ca8c", size = 10412965, upload-time = "2026-05-11T18:52:41.915Z" }, + { url = "https://files.pythonhosted.org/packages/65/b6/09b01cdbc15224e2850365192d17b7bdebb8bdbd8780ed221fcdf0d9a515/pandas-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6dc0b3fd2169c9157deed50b4d519553a3655c8c6a96027136d654592be973a9", size = 10894600, upload-time = "2026-05-11T18:52:45.02Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a4/2eb28f2fccb4ced4a2c79ab2a5dee9ade1ebf44922ebad6fea158c9f95d4/pandas-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e65d5407dc0b394f509699650e4a2ec01c0514f21850f453fa60f3be79a5dbf", size = 11422824, upload-time = "2026-05-11T18:52:48.058Z" }, + { url = "https://files.pythonhosted.org/packages/f8/45/830bb57f533a4604b355e07edcb8ea18cf88b5f94e5fca92f27052d7c597/pandas-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8894dc474d648fe7b6ff0ca9b0bd73950d19952bc1a6534540762c5d79d305c", size = 11950889, upload-time = "2026-05-11T18:52:50.905Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c5/fc1b368f303087d20e8c9bf3d6ceb186263cfac0ade735cd938538bea839/pandas-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:c7be265b62cef88e253a941e4698604973736dcfe242fdb5198f0f7bc473cdcc", size = 9755463, upload-time = "2026-05-11T18:52:53.386Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/fda8f9705b1b09c6ebe14bfc0fa0e4ec8584d54ea673628f157ff55131af/pandas-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:557409bc4178e70ee8d9ddb494798e51ebf6ea59330f6be22c51bab2a7db6c49", size = 9066158, upload-time = "2026-05-11T18:52:56.038Z" }, + { url = "https://files.pythonhosted.org/packages/c5/90/62d8302883c44308c477e222c3daf7c813a34c8e96985882fbd53d964352/pandas-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67b3b64c11910cfa29f4e94a14d3bff9ee693b6fc76055e7cad549cee0aec5fa", size = 10331071, upload-time = "2026-05-11T18:52:58.838Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ae/6a6493c783a101f165e4356953ba3c74d6f77f0042fa7d753da9dfbb640c/pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7", size = 9875690, upload-time = "2026-05-11T18:53:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/62/7c/5df8e9f56c69a2769fbe9382a5ef8f2658c007e376434e1e2cbb57ad895f/pandas-3.0.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4be06d68f9ddcfc645b87534911da79a8fbffc7573c80e0edcf42a5020624d8", size = 10381634, upload-time = "2026-05-11T18:53:04.393Z" }, + { url = "https://files.pythonhosted.org/packages/99/68/1237369725aa617bb358263d535803e3053fdbc593513ec5ed9c9896b5b6/pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a", size = 10891243, upload-time = "2026-05-11T18:53:07.643Z" }, + { url = "https://files.pythonhosted.org/packages/25/93/77d108e8af7222b4a503ebde0e30215b1c2e4f8e53a526431890f22d5586/pandas-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1928e07221f82db493cd4af1e23c1bfca524a19a4699887975bff68f49a72bfb", size = 11388659, upload-time = "2026-05-11T18:53:10.634Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bd/eff5b4399f332ac386c853f6cd2bd3fa2ca0061b9f36ecd9c4d7c4265649/pandas-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51b1fe551acb77dac643c6fda86084d8d446c10fe64b06a9cc29c4cc8540e7f2", size = 11942880, upload-time = "2026-05-11T18:53:13.536Z" }, + { url = "https://files.pythonhosted.org/packages/2c/20/559ace4200982c3887d0b86bfd0d856a2143ef8ddab63cc07934951a964c/pandas-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44", size = 9757091, upload-time = "2026-05-11T18:53:16.306Z" }, + { url = "https://files.pythonhosted.org/packages/3a/66/69055a09fe200f29f922a3eeec4804611900b95f52d932ece3393c3c0c19/pandas-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:275c14e0fce14a2ec20eee474aecd305478ea3c1e6f6a9d8fe219a165542717e", size = 9057282, upload-time = "2026-05-11T18:53:18.768Z" }, + { url = "https://files.pythonhosted.org/packages/57/0e/efe801b0e6811e8e650cd21b7f2608e30f08a7067e2bf6e8752b0d56ee3c/pandas-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:46997386d528eb40376ecd6b033cf4a8a1e5282580f68f43de875b78cba2199d", size = 10767016, upload-time = "2026-05-11T18:53:21.227Z" }, + { url = "https://files.pythonhosted.org/packages/ea/dc/eb55135a1d5f0f0519f28da1f609a206d2cad1f9c35c32d51e38dd7261ae/pandas-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261e308dfb22448384b7580cf719d2f998fe2966c92893c3e77d14008af1f066", size = 10420210, upload-time = "2026-05-11T18:53:23.982Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3e/b1d5d955ce33ffecb407465a60bc32769d74fcf68224b7ae67ae11d4dea4/pandas-3.0.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd1a5d1def6a46002e964510bdc67c368aa0951df5d1d9f8365336f5a1f490cd", size = 10336126, upload-time = "2026-05-11T18:53:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/a01261711ab60a22d71b862f0de20e4c504bf80457270ad8cb42110f6abc/pandas-3.0.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d72828c20c6d6e83e1e22a6a3b47b326b71664112fa9705dcbccfd7a39b62085", size = 10728051, upload-time = "2026-05-11T18:53:29.125Z" }, + { url = "https://files.pythonhosted.org/packages/e9/21/ea191195e587b18cf682e97f433f81b2d0fbe341380e80a3e0d6e4403c8e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870", size = 11350796, upload-time = "2026-05-11T18:53:32.056Z" }, + { url = "https://files.pythonhosted.org/packages/64/69/f0eaaf54939f0e8c6768fd06be9af2cef9b36048b96dfb9e1b2c685a807e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f", size = 11799741, upload-time = "2026-05-11T18:53:34.985Z" }, + { url = "https://files.pythonhosted.org/packages/45/a4/865e0e510cae5fc2194de4db28be638952de942571ba9125934fd9c01d47/pandas-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13", size = 10499958, upload-time = "2026-05-11T18:53:37.857Z" }, + { url = "https://files.pythonhosted.org/packages/86/54/effdcc3c0ff7a08037889200e148ebe94c16c4f653be078c7b3675955df1/pandas-3.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3650109c0f22879df8bd6179ab9ee3d7f1d1d4e7e0094a3f0032d9f51e2e64ac", size = 10336065, upload-time = "2026-05-11T18:53:41.099Z" }, + { url = "https://files.pythonhosted.org/packages/68/10/bf2d6738d72748b961a3751ab89522d58c54efc36a8e1a12161216cd45cf/pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f", size = 9926101, upload-time = "2026-05-11T18:53:43.515Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e9/e35cf11c8a136e757b956f5f0efdcaa50aecde85ea055f1898dfc68262f3/pandas-3.0.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba7e08b9ac1d54569cd1e256e3668975ed624d6826f7b68df0342b012007bddb", size = 10457553, upload-time = "2026-05-11T18:53:46.394Z" }, + { url = "https://files.pythonhosted.org/packages/58/3b/1cdec6772bdbaf7b25dab360c59f03cadf05492dd724c6540af905389b07/pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a", size = 10914065, upload-time = "2026-05-11T18:53:49.134Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/1ef644445fcd72e3627bceec77e3560636f87ddce4ed841afe76b83b5bf9/pandas-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3a2ec42c98ffa2565a67e08e218d06d72576d758d90facb7c00805194d8f360", size = 11459188, upload-time = "2026-05-11T18:53:52.527Z" }, + { url = "https://files.pythonhosted.org/packages/7e/49/4d8d4f42cbc9c4adc7a1870f269c02cbd6cd40d059622c06fb298addcbad/pandas-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:335f62418ed562cfc3c49e9e196375c28b729dcef8543abf4f9438e381bf3c76", size = 11982966, upload-time = "2026-05-11T18:53:55.043Z" }, + { url = "https://files.pythonhosted.org/packages/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5", size = 9876755, upload-time = "2026-05-11T18:53:58.067Z" }, + { url = "https://files.pythonhosted.org/packages/2a/af/33c469653b0ba03b50c3a98192d4c07f0c75c66b263ceb097fce0ee97d31/pandas-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:a2d2dff8a04f3917b55ab3910c32990f8ddf7eceba114947838cefa976a68977", size = 9198658, upload-time = "2026-05-11T18:54:00.733Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fa/b8c257bd76b8bd060c3a9151c1fca05e9b9c5e3af5d0f549c0356f6d143d/pandas-3.0.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0d589105b3c14645af1738ff279b2995102d8f7a03b0a66dc8d95550eb513e04", size = 10787242, upload-time = "2026-05-11T18:54:03.564Z" }, + { url = "https://files.pythonhosted.org/packages/54/eb/f19206ffb0bf1919002969aa448b4702c6594845156a6f8050674855aac3/pandas-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13fc1e853d9e04743d11ba75a985ccbc2a317fe07d8af61e445a6fd24dacd6a6", size = 10436369, upload-time = "2026-05-11T18:54:06.311Z" }, + { url = "https://files.pythonhosted.org/packages/fd/24/c7c39fb4fe22b71a0c2d78bf0c585c600092d85f94f086d2b3b2f6ca27e2/pandas-3.0.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:819959dab7bbd0049c15623fbac4e29a191b9528160a61fb1032242d8ced2d9c", size = 10358306, upload-time = "2026-05-11T18:54:09.085Z" }, + { url = "https://files.pythonhosted.org/packages/16/ec/dd2a9eb7fa1204df88c0864164e35b228ac581062ac612ba0a67fd812e4c/pandas-3.0.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60ae316d3fd75d1858d450d0db0103ea2be3e7d4a95ec2f064f7e2ae63f7b028", size = 10758394, upload-time = "2026-05-11T18:54:11.956Z" }, + { url = "https://files.pythonhosted.org/packages/95/6e/00c61ea8e85b4f6d8d35e11852a1a4998fc7fafc91c6a602d1cc9c972d64/pandas-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd3a518890b400d32f9023722dc9a9a5c969f00b415419a3c06c043f09bb5d7d", size = 11375717, upload-time = "2026-05-11T18:54:14.539Z" }, + { url = "https://files.pythonhosted.org/packages/31/89/8fc1c268969fac43688d65fd92e67df24bd128d53cb4d2eee534cd307399/pandas-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c39be2d709d01fa972a0cabc522389fceca4f3969332ba25a7d6c5802cf976a", size = 11828897, upload-time = "2026-05-11T18:54:17.146Z" }, + { url = "https://files.pythonhosted.org/packages/56/3b/e7d20dea247a3e6dc0bd8a6953854afbedc03951def4e7371e05e7263e25/pandas-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4db8c527972a821cf5286b40ccc57642a39bc62e62022b42f99f8a67fca8c3a1", size = 10900855, upload-time = "2026-05-11T18:54:19.72Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/68a0978d1ef8502b8492099beaa6e7a0c1b32e3b5d4f677f5810cb08711c/pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1", size = 9466464, upload-time = "2026-05-11T18:54:22.754Z" }, ] [[package]] @@ -947,11 +947,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.9.6" +version = "4.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, + { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, ] [[package]] @@ -974,15 +974,15 @@ wheels = [ [[package]] name = "pymdown-extensions" -version = "10.21.2" +version = "11.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/08/f1c908c581fd11913da4711ea7ba32c0eee40b0190000996bb863b0c9349/pymdown_extensions-10.21.2.tar.gz", hash = "sha256:c3f55a5b8a1d0edf6699e35dcbea71d978d34ff3fa79f3d807b8a5b3fa90fbdc", size = 853922, upload-time = "2026-03-29T15:01:55.233Z" } +sdist = { url = "https://files.pythonhosted.org/packages/21/a9/5f0c535ba3b08fe09270c16808e053a968868242ecbd5676d4e3a488bf28/pymdown_extensions-11.0.1.tar.gz", hash = "sha256:dd2905ae6fc5b75582fafb139a1266ffc754705efa902aa50067fa7ff4f94ec0", size = 857113, upload-time = "2026-07-02T17:59:22.955Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl", hash = "sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638", size = 268901, upload-time = "2026-03-29T15:01:53.244Z" }, + { url = "https://files.pythonhosted.org/packages/d6/54/da572c98c0b77626a91b5d3b89f0231d8bff5125c225420908632f8b342d/pymdown_extensions-11.0.1-py3-none-any.whl", hash = "sha256:db3943a62bab7e03af1364f0c4083e64b91fb097675a4b6cceccfbe9a77e5eb2", size = 269455, upload-time = "2026-07-02T17:59:21.271Z" }, ] [[package]] @@ -996,7 +996,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.3" +version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1005,9 +1005,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] [[package]] @@ -1105,7 +1105,7 @@ wheels = [ [[package]] name = "requests" -version = "2.33.1" +version = "2.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -1113,34 +1113,34 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] [[package]] name = "ruff" -version = "0.15.12" +version = "0.15.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/99/43/3291f1cc9106f4c63bdce7a8d0df5047fe8422a75b091c16b5e9355e0b11/ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6", size = 4643852, upload-time = "2026-04-24T18:17:14.305Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/06/ae069393fc66e8ff33036d4b368003833bf6e88ccf182e17e7a2f1c754fd/ruff-0.15.22.tar.gz", hash = "sha256:3f15175b1fb580126f58285a5dae6b2ea89000136d980c64499211f116b54809", size = 4785063, upload-time = "2026-07-16T15:14:13.244Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/6e/e78ffb61d4686f3d96ba3df2c801161843746dcbcbb17a1e927d4829312b/ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c", size = 10640713, upload-time = "2026-04-24T18:17:22.841Z" }, - { url = "https://files.pythonhosted.org/packages/ae/08/a317bc231fb9e7b93e4ef3089501e51922ff88d6936ce5cf870c4fe55419/ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c", size = 11069267, upload-time = "2026-04-24T18:17:30.105Z" }, - { url = "https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5", size = 10397182, upload-time = "2026-04-24T18:17:07.177Z" }, - { url = "https://files.pythonhosted.org/packages/71/e0/3310fc6d1b5e1fdea22bf3b1b807c7e187b581021b0d7d4514cccdb5fb71/ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002", size = 10758012, upload-time = "2026-04-24T18:16:55.759Z" }, - { url = "https://files.pythonhosted.org/packages/11/c1/a606911aee04c324ddaa883ae418f3569792fd3c4a10c50e0dd0a2311e1e/ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5", size = 10447479, upload-time = "2026-04-24T18:16:51.677Z" }, - { url = "https://files.pythonhosted.org/packages/9d/68/4201e8444f0894f21ab4aeeaee68aa4f10b51613514a20d80bd628d57e88/ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6", size = 11234040, upload-time = "2026-04-24T18:17:16.529Z" }, - { url = "https://files.pythonhosted.org/packages/34/ff/8a6d6cf4ccc23fd67060874e832c18919d1557a0611ebef03fdb01fff11e/ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33", size = 12087377, upload-time = "2026-04-24T18:17:04.944Z" }, - { url = "https://files.pythonhosted.org/packages/85/f6/c669cf73f5152f623d34e69866a46d5e6185816b19fcd5b6dd8a2d299922/ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847", size = 11367784, upload-time = "2026-04-24T18:17:25.409Z" }, - { url = "https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0", size = 11344088, upload-time = "2026-04-24T18:17:12.258Z" }, - { url = "https://files.pythonhosted.org/packages/c2/8d/49afab3645e31e12c590acb6d3b5b69d7aab5b81926dbaf7461f9441f37a/ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339", size = 11271770, upload-time = "2026-04-24T18:17:02.457Z" }, - { url = "https://files.pythonhosted.org/packages/46/06/33f41fe94403e2b755481cdfb9b7ef3e4e0ed031c4581124658d935d52b4/ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5", size = 10719355, upload-time = "2026-04-24T18:17:27.648Z" }, - { url = "https://files.pythonhosted.org/packages/0d/59/18aa4e014debbf559670e4048e39260a85c7fcee84acfd761ac01e7b8d35/ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd", size = 10462758, upload-time = "2026-04-24T18:17:32.347Z" }, - { url = "https://files.pythonhosted.org/packages/25/e7/cc9f16fd0f3b5fddcbd7ec3d6ae30c8f3fde1047f32a4093a98d633c6570/ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b", size = 10953498, upload-time = "2026-04-24T18:17:20.674Z" }, - { url = "https://files.pythonhosted.org/packages/72/7a/a9ba7f98c7a575978698f4230c5e8cc54bbc761af34f560818f933dafa0c/ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e", size = 11447765, upload-time = "2026-04-24T18:17:09.755Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f9/0ae446942c846b8266059ad8a30702a35afae55f5cdc54c5adf8d7afdc27/ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20", size = 10657277, upload-time = "2026-04-24T18:17:18.591Z" }, - { url = "https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d", size = 11837758, upload-time = "2026-04-24T18:17:00.113Z" }, - { url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" }, + { url = "https://files.pythonhosted.org/packages/23/18/ee54b7ae1e121be7a28ea6da4b67564ebb0530e183a54415ab7e3bcd2c4e/ruff-0.15.22-py3-none-linux_armv6l.whl", hash = "sha256:44423e73493737f5e7c5b41d475483898ff37afcdae38bc3da5085e29af1c2d8", size = 10781258, upload-time = "2026-07-16T15:13:19.452Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d2/2520cb14761ddbeaf57642a76942fc36adcbdbe53b4532241995f6fc485c/ruff-0.15.22-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b82c6482946e9eda7ff2e091d25b8bad3f718684e1916d41bd56873cee05b697", size = 10999477, upload-time = "2026-07-16T15:13:23.318Z" }, + { url = "https://files.pythonhosted.org/packages/c9/10/74e53572aa758dfaa678c2a2646b5c5515d884b7ca56be4d2ce03ca4b560/ruff-0.15.22-py3-none-macosx_11_0_arm64.whl", hash = "sha256:11c1c715af53a09f714e011106bffc419751ec8232fcb5da42173284ea3fec6f", size = 10466716, upload-time = "2026-07-16T15:13:26.162Z" }, + { url = "https://files.pythonhosted.org/packages/1e/cc/44eaaf0844e028182f2d0a8f2190d0f359159aed0a9e5ab861d892f1ae2a/ruff-0.15.22-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:742a29cf29bddb7c8327895d6a10e0e6c5b38a96dd407af9b5d0857f809c0576", size = 10892644, upload-time = "2026-07-16T15:13:29.229Z" }, + { url = "https://files.pythonhosted.org/packages/9f/21/8edf559014d2b0f82beea19cfb713993ad802ccda16868769979c6090a84/ruff-0.15.22-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72af58b951b0ae395935ae79763dc349bc0eb706319d28f7a33ad2cfb3cfc178", size = 10576719, upload-time = "2026-07-16T15:13:32.35Z" }, + { url = "https://files.pythonhosted.org/packages/bf/1e/3a13abd392a3b50b62e5938a831f9ab6e588358cacad5c18545b716d2182/ruff-0.15.22-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d425005c1835eb24e2ee4161cb90e8db263415f4a71c8c72c33abaa6c0c224", size = 11376494, upload-time = "2026-07-16T15:13:35.958Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3e/422d3d95bcf04dd78e1aeac22184d4f9a8fb2c01865d39d44618484a0317/ruff-0.15.22-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b9b3f8779a4f08c969defc3c8c35abffaa757e601ed5ae66d6d1db6519969a", size = 12208370, upload-time = "2026-07-16T15:13:39.185Z" }, + { url = "https://files.pythonhosted.org/packages/1e/91/5d065a0e0a02bf4813f5119ad278462eed081d2b832eb7c021ade0ec9e65/ruff-0.15.22-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e0dd1b2e4d3d585f897a0d137cbf4eaf6223bef4e8ce34d6bb12556c5f9249e", size = 11581098, upload-time = "2026-07-16T15:13:42.132Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f9/a0d4871d12fae702eb1f41b686caf05f1f8b124dc6db6f784f53d74918fa/ruff-0.15.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365523eb91d9224e1bcb03b022fbf0facb8f9e23792a2c53d9d4b3924bdbdebb", size = 11399422, upload-time = "2026-07-16T15:13:45.2Z" }, + { url = "https://files.pythonhosted.org/packages/18/80/c843a5176cddbceb0b7e8dd41cf9993490796c1c469348d384f5a5c13c56/ruff-0.15.22-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fabfd168afdf29fee5be98b831efa9683c94d7c5a3b58b9ce5a2e38444589a74", size = 11381683, upload-time = "2026-07-16T15:13:48.46Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/8485de0ae92239438a36cfc51350db9b9e85c9ebdfaea91b18e422706662/ruff-0.15.22-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:225dbf095a87f1d9f90f5fd7924d2613ee452a75a4308c63a8f50f761787aa7c", size = 10850295, upload-time = "2026-07-16T15:13:51.655Z" }, + { url = "https://files.pythonhosted.org/packages/fa/91/24977ec2ec72eaf15e4394ace2959fdff2dd1e14f03e005e838023407169/ruff-0.15.22-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1877d63b9d24ed278744f1523fd11b85540566d54641f97c566d7d9dc5ca5296", size = 10579640, upload-time = "2026-07-16T15:13:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/9c/47/9b51216951974df1f263ac19da550d34252e0ed7218c25f10c5ef9ed7517/ruff-0.15.22-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1606c510bd7215680d32efab38965f7cdec3ef69f5170a3f4791404ffdd5262", size = 11105077, upload-time = "2026-07-16T15:13:57.915Z" }, + { url = "https://files.pythonhosted.org/packages/c2/47/20e9d4a3b8016778acea5fc32bb50d35d207500a17ddb529ffa6996feef8/ruff-0.15.22-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:630479b18625f5ffc373f77603a22a9f8ac0acd7ff0501178b5db28ec71e9c64", size = 11490980, upload-time = "2026-07-16T15:14:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/4d/76/3f72d8fc38c1cb77b38c56a70da9d0c17700cc1cc50f9649c9d3c8f5ba71/ruff-0.15.22-py3-none-win32.whl", hash = "sha256:e5ba0e4a13fd14abbed2a77b517a3911290c6c6c59ef67784328d1668fab76cf", size = 10789165, upload-time = "2026-07-16T15:14:04.16Z" }, + { url = "https://files.pythonhosted.org/packages/cb/46/4965251734c2b6fcdca1b1b187d20bcac3af0ee5b083b89c910bb961ce3a/ruff-0.15.22-py3-none-win_amd64.whl", hash = "sha256:9be63ba1eb936acd2d1342fb8337c356353706fce233b2a15a09a97037e6acde", size = 11938297, upload-time = "2026-07-16T15:14:07.316Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/e69b1ff4c8b69093ef08b8919ab767af0569666865b39c30a8795d88d3c6/ruff-0.15.22-py3-none-win_arm64.whl", hash = "sha256:e1168075b72158510839f250027659cdd78476f40507dd517892304c41318661", size = 11298172, upload-time = "2026-07-16T15:14:10.51Z" }, ] [[package]] @@ -1152,15 +1152,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] -[[package]] -name = "smmap" -version = "5.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, -] - [[package]] name = "tomli" version = "2.4.1" @@ -1217,53 +1208,54 @@ wheels = [ [[package]] name = "ty" -version = "0.0.32" +version = "0.0.60" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/7e/2aa791c9ae7b8cd5024cd4122e92267f664ca954cea3def3211919fa3c1f/ty-0.0.32.tar.gz", hash = "sha256:8743174c5f920f6700a4a0c9de140109189192ba16226884cd50095b43b8a45c", size = 5522294, upload-time = "2026-04-20T19:29:01.626Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/6e/1ab6f2727622d38ddfb2a7f994209b3087190b76885e2f754dbb6e58e0c9/ty-0.0.60.tar.gz", hash = "sha256:ebd7517d1aa8d8c3793cbf03c263679a42b939eca650df583234f92a5eb5837a", size = 6189323, upload-time = "2026-07-16T10:18:14.124Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/eb/1075dc6a49d7acbe2584ae4d5b410c41b1f177a5adcc567e09eca4c69000/ty-0.0.32-py3-none-linux_armv6l.whl", hash = "sha256:dacbc2f6cd698d488ae7436838ff929570455bf94bfa4d9fe57a630c552aff83", size = 10902959, upload-time = "2026-04-20T19:28:31.907Z" }, - { url = "https://files.pythonhosted.org/packages/33/d2/c35fc8bc66e98d1ee9b0f8ed319bf743e450e1f1e997574b178fab75670f/ty-0.0.32-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914bbc4f605ce2a9e2a78982e28fae1d3359a169d141f9dc3b4c7749cd5eca81", size = 10726172, upload-time = "2026-04-20T19:28:44.765Z" }, - { url = "https://files.pythonhosted.org/packages/96/32/c827da3ca480456fb02d8cea68a2609273b6c220fea0be9a4c8d8470b86e/ty-0.0.32-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4787ac9fe1f86b1f3133f5c6732adbe2df5668b50c679ac6e2d98cd284da812f", size = 10163701, upload-time = "2026-04-20T19:28:27.005Z" }, - { url = "https://files.pythonhosted.org/packages/ba/9e/2734478fbdb90c160cb2813a3916a16a2af5c1e231f87d635f6131d781fb/ty-0.0.32-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ea0a728af99fe40dd744cba6441a2404f80b7f4bde17aa6da393810af5ea57", size = 10656220, upload-time = "2026-04-20T19:29:03.814Z" }, - { url = "https://files.pythonhosted.org/packages/44/9f/0007da2d35e424debe7e9f86ffbc1ab7f60983cfbc5f0411324ab2de5292/ty-0.0.32-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2850561f9b018ae33d7e5bbfa0ac414d3c518513edcffe43877dc9801446b9c5", size = 10696086, upload-time = "2026-04-20T19:28:46.829Z" }, - { url = "https://files.pythonhosted.org/packages/3b/5e/ce5fd4ec803222ae3e69a76d2a2db2eed55e19f5b131702b9789ef45f93d/ty-0.0.32-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b5fa2fb3c614349ee211d36476b49d88c5ef79a687cdb91b2872ad023b94d2f8", size = 11184800, upload-time = "2026-04-20T19:28:42.57Z" }, - { url = "https://files.pythonhosted.org/packages/6c/46/ebcf67a5999421331214aac51a7464db42de2be15bbe929c612a3ed0b039/ty-0.0.32-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b89969307ab2417d41c9be8059dd79feea577234e1e10d35132f5495e0d42c6", size = 11718718, upload-time = "2026-04-20T19:28:36.433Z" }, - { url = "https://files.pythonhosted.org/packages/18/2c/2141c86ed0ce0962b45cefb658a95e734f59759d47f20afdcd9c732910a1/ty-0.0.32-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b59868ede9b1d69a088f0d695df52a0061f95fa7baa1d5e0dc6fc9cf06e1334", size = 11346369, upload-time = "2026-04-20T19:28:48.967Z" }, - { url = "https://files.pythonhosted.org/packages/7a/da/ed6f772339cf29bd9a46def9d6db5084689eb574ee4d150ff704224c1ed8/ty-0.0.32-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8300caf35345498e9b9b03e550bba03cee8f5f5f8ab4c83c3b1ff1b7403b7d3a", size = 11280714, upload-time = "2026-04-20T19:28:51.516Z" }, - { url = "https://files.pythonhosted.org/packages/da/9b/c6813987edf4816a40e0c8e408b555f97d3f267c7b3a1688c8bbdf65609c/ty-0.0.32-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:583c7094f4574b02f724db924f98b804d1387a0bd9405ecb5e078cc0f47fbcfb", size = 10638806, upload-time = "2026-04-20T19:28:29.651Z" }, - { url = "https://files.pythonhosted.org/packages/4e/d4/0cefcbd2ad0f3d51762ccf58e652ec7da146eb6ae34f87228f6254bbb8be/ty-0.0.32-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e44ebe1bb4143a5628bc4db67ac0dfebe14594af671e4ee66f6f2e983da56501", size = 10726106, upload-time = "2026-04-20T19:29:06.3Z" }, - { url = "https://files.pythonhosted.org/packages/32/ad/2c8a97f91f06311f4367400f7d13534bbda2522c73c99a3e4c0757dff9b8/ty-0.0.32-py3-none-musllinux_1_2_i686.whl", hash = "sha256:06f17ada3e069cba6148342ef88e9929156beca8473e8d4f101b68f66c75643e", size = 10872951, upload-time = "2026-04-20T19:28:34.077Z" }, - { url = "https://files.pythonhosted.org/packages/ba/68/42293f9248106dd51875120971a5cc6ea315c2c4dcfb8e59aa063aa0af26/ty-0.0.32-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e96e60fa556cec04f15d7ea62d2ceee5982bd389233e961ab9fd42304e278175", size = 11363334, upload-time = "2026-04-20T19:28:54.036Z" }, - { url = "https://files.pythonhosted.org/packages/df/92/be9abf4d3e589ad5023e2ea965b93e204ec856420d46adf73c5c36c04678/ty-0.0.32-py3-none-win32.whl", hash = "sha256:2ff2ebb4986b24aebcf1444db7db5ca41b36086040e95eea9f8fb851c11e805c", size = 10260689, upload-time = "2026-04-20T19:28:56.541Z" }, - { url = "https://files.pythonhosted.org/packages/14/61/dc86acea899349d2579cb8419aecedd83dc504d7d6a10df65eef546c8300/ty-0.0.32-py3-none-win_amd64.whl", hash = "sha256:ba7284a4a954b598c1b31500352b3ec1f89bff533825592b5958848226fdc7ee", size = 11255371, upload-time = "2026-04-20T19:28:39.917Z" }, - { url = "https://files.pythonhosted.org/packages/43/01/beffec56d71ca25b343ede63adb076456b5b3e211f1c066452a44cd120b3/ty-0.0.32-py3-none-win_arm64.whl", hash = "sha256:7e10aadbdbda989a7d567ee6a37f8b98d4d542e31e3b190a2879fd581f75d658", size = 10658087, upload-time = "2026-04-20T19:28:59.286Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fc/c82d30b753dfb9d83ac34568478d9487bc42e1e79241a22b57f4b28fabee/ty-0.0.60-py3-none-linux_armv6l.whl", hash = "sha256:0842270c2e10d8416ca9f8aca1357c827efc5212300fbbc709e186e66fc7f9da", size = 11831443, upload-time = "2026-07-16T10:17:34.912Z" }, + { url = "https://files.pythonhosted.org/packages/47/d4/09e386d816076d1d90c57baee7f607e1475b625bb9f9279b28302da22f18/ty-0.0.60-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f49d024571cd7e569081638ab0ccc9e6795d642c6b9208addecc5e487364f0a3", size = 11624536, upload-time = "2026-07-16T10:17:37.445Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1e/e527e29fd9a3d41ddc419a43b37927c42a0d8ee4ca8a148ce4e022328f2f/ty-0.0.60-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5b006c9a793d735ff808999775bbc0f07f91de50e931799f17322440cd9d055f", size = 11032920, upload-time = "2026-07-16T10:17:39.695Z" }, + { url = "https://files.pythonhosted.org/packages/79/e0/0f294bf2521f2836ba1a4682d4301c2038e4dfedee3a70df8f6005694978/ty-0.0.60-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:233033de40a0722420380d40f0eeebe8eb0d3afeeab3a6e7b1388e2caf1a017c", size = 11575334, upload-time = "2026-07-16T10:17:41.644Z" }, + { url = "https://files.pythonhosted.org/packages/0c/8c/682e46e29165d9c94829576687092a8ea4556dd5882b11edd32bc7f4b534/ty-0.0.60-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61bcbd67f653ae2966d0e423879127907c6f8b567d64b6db93cb8f6cca7045ed", size = 11659354, upload-time = "2026-07-16T10:17:43.815Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d3/6baf7eb3cf9459416032285a10ffe1309f7e255e3f1974372bc41d44cfd3/ty-0.0.60-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b72e44a0be08ad778693d49b00b03bb784c1465d8265b3b392b355b34257a437", size = 12293710, upload-time = "2026-07-16T10:17:46.032Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f7/b287d78c93449fc5153891ad040be55e0718711ae7c0f2c54d3b88dd4d00/ty-0.0.60-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13b89f03a76f149350617345ae33bd332d2c6023dc5d560dd10a4f0210fba13a", size = 12837636, upload-time = "2026-07-16T10:17:48.453Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f2/3d8c77ca5f836fb6280231030df5a4504476a38d4554330d2be42125888e/ty-0.0.60-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30db8ebf6715da30afac62311ac773bf436ed503ed989cc9d4be4d3bdc3cbd43", size = 12383469, upload-time = "2026-07-16T10:17:50.567Z" }, + { url = "https://files.pythonhosted.org/packages/e8/98/958047501d74f9d0df357df28fea1de2afa18f1e6b2dc4a3ea4975c83e14/ty-0.0.60-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ecd6dc686f90c3bfe22c9b435e4a5325f5066f706bfe95c1616ef425b1f0654", size = 12132334, upload-time = "2026-07-16T10:17:52.718Z" }, + { url = "https://files.pythonhosted.org/packages/8c/8f/b2853634da28aac01ea0ba8ef7695dc3d6c9aaf9c63dfdcf7c7435bc6c25/ty-0.0.60-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a2292124558d839e31afd9829b11dda726ce65096b1feb80d14a1ddc7ccb0f28", size = 12359945, upload-time = "2026-07-16T10:17:55.143Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ae/bfecc71fcf1b833117bfb0efb76e14f5772874775274115ac94f64c5ba7a/ty-0.0.60-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:78812ec03da2c3141030ee1717fb09f0845eb552a77e79680c1cca598861ddaf", size = 11534812, upload-time = "2026-07-16T10:17:57.165Z" }, + { url = "https://files.pythonhosted.org/packages/d6/23/1236c1ab9cd6b2daba9c558c9dcf04644f7b636f3bab3c15e74c7f80f5e1/ty-0.0.60-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a92c1dbc48f8b414ddcf199c1119054de00f8ee995528fa9d9c2e4ddd13cd0c1", size = 11677591, upload-time = "2026-07-16T10:17:59.747Z" }, + { url = "https://files.pythonhosted.org/packages/49/49/86c9230d9a8bf1755ca4b0165e7cb6bdfa2e495f0a105b24c6a27b5a542c/ty-0.0.60-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2c367d5ffe545004f0603e5599614062230678eeab980acd32850a7c19d60fc9", size = 11901279, upload-time = "2026-07-16T10:18:02.301Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0a/0c2e9904362d9c63700116feb192a3e10122eef49a417cde84068b2562fd/ty-0.0.60-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a94b1f99aa8f8ca5879341a5351af708b85ce19c7494db320c719a1e129d08e8", size = 12230659, upload-time = "2026-07-16T10:18:04.384Z" }, + { url = "https://files.pythonhosted.org/packages/84/4d/39c414f6b4bc944b2bac19c181438ee393c8eb34e9e21d97e5a148745f95/ty-0.0.60-py3-none-win32.whl", hash = "sha256:5ea2c85249581fb0060b9ff63b5313330f48930b2e50f6c85a5a322701626cc5", size = 11175286, upload-time = "2026-07-16T10:18:06.523Z" }, + { url = "https://files.pythonhosted.org/packages/a7/08/fdf4072d96c71b65fe98c9ca2ea5b3d2d0f6e20873bb2639e8c7827bd5f6/ty-0.0.60-py3-none-win_amd64.whl", hash = "sha256:15d83c3d793cb07840b8888c084cc2c49eb8acc29456a2fcdfbfd509c82526e5", size = 12249240, upload-time = "2026-07-16T10:18:08.701Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a2/83a496d717f712f894cf26690bcd9465ca23cd1c9139cac669d9cca45d14/ty-0.0.60-py3-none-win_arm64.whl", hash = "sha256:32e63228c3d21ddb192385aaf54501f19478be7b764f7572014d5110e53a9085", size = 11620140, upload-time = "2026-07-16T10:18:11.316Z" }, ] [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] [[package]] name = "tzdata" -version = "2026.2" +version = "2026.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, + { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, ] [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]] @@ -1295,12 +1287,12 @@ wheels = [ [[package]] name = "wcmatch" -version = "10.1" +version = "11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "bracex" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/79/3e/c0bdc27cf06f4e47680bd5803a07cb3dfd17de84cde92dd217dcb9e05253/wcmatch-10.1.tar.gz", hash = "sha256:f11f94208c8c8484a16f4f48638a85d771d9513f4ab3f37595978801cb9465af", size = 117421, upload-time = "2025-06-22T19:14:02.49Z" } +sdist = { url = "https://files.pythonhosted.org/packages/16/25/1da725838132221e33568973da484ff43813662ccc06ebf7f6e3abddfcd5/wcmatch-11.0.tar.gz", hash = "sha256:55d95c2447789712774b198ceec72939e88b5618f1f8f0a9b605bf7740b63b96", size = 141360, upload-time = "2026-07-10T05:50:24.183Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/d8/0d1d2e9d3fabcf5d6840362adcf05f8cf3cd06a73358140c3a97189238ae/wcmatch-10.1-py3-none-any.whl", hash = "sha256:5848ace7dbb0476e5e55ab63c6bbd529745089343427caa5537f230cc01beb8a", size = 39854, upload-time = "2025-06-22T19:14:00.978Z" }, + { url = "https://files.pythonhosted.org/packages/28/12/f38b6fee116274d7221743caab07d765032e1370bb54cad8714f87aeb0e8/wcmatch-11.0-py3-none-any.whl", hash = "sha256:3a5977ace27e075eef67eb03d539563f1a19018b62881949a42932cf66926934", size = 42914, upload-time = "2026-07-10T05:50:22.995Z" }, ] From abf6a8b9375ff9f6621a26a7a0431cbca1528b62 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 17 Jul 2026 11:28:27 -0700 Subject: [PATCH 256/267] Add codespell for CICD --- .github/workflows/pyharp.yml | 3 +++ pyproject.toml | 5 +++++ uv.lock | 11 +++++++++++ 3 files changed, 19 insertions(+) diff --git a/.github/workflows/pyharp.yml b/.github/workflows/pyharp.yml index 40b3a80..8e240cb 100644 --- a/.github/workflows/pyharp.yml +++ b/.github/workflows/pyharp.yml @@ -29,6 +29,9 @@ jobs: - name: Install python dependencies run: uv sync --group dev + - name: Run codespell + run: uv run codespell + - name: Run ruff format run: uv run ruff format --check diff --git a/pyproject.toml b/pyproject.toml index 1001721..f01826f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,6 +76,7 @@ docs = [ ] dev = [ + "codespell>=2.3.0", "pytest>=8.3.5", "pytest-cov>=6.1.1", "ruff>=0.11.0", @@ -88,6 +89,10 @@ dev = [ "typing-extensions>=4.15.0", ] +[tool.codespell] +skip = "*.lock,./.venv,./site,./.git,./.mypy_cache,./.ruff_cache,./.pytest_cache,*.pyc" +check-filenames = true + [tool.ruff.lint.pydocstyle] convention = "numpy" diff --git a/uv.lock b/uv.lock index 7f9820e..d465ba6 100644 --- a/uv.lock +++ b/uv.lock @@ -149,6 +149,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, ] +[[package]] +name = "codespell" +version = "2.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/80/19/45e941380f69c042b43423513d201e6592346f992394347f5e7174c31407/codespell-2.4.3.tar.gz", hash = "sha256:cbe085e331227b37bb86ef8bddd08dc768c704ee9a07ca869852c093fa2793e2", size = 352773, upload-time = "2026-07-15T11:51:54.159Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/bf/bdb951d34eb169140b546f44be9ec4525d1acefb9eb5572071f5492b19fc/codespell-2.4.3-py3-none-any.whl", hash = "sha256:af2505b335e8573dbd2d384d1c4ef498f4006f4ba2d6fceca01e55b91f52628a", size = 340736, upload-time = "2026-07-15T11:51:52.925Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -306,6 +315,7 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "codespell" }, { name = "harp-benchmarks" }, { name = "harp-data" }, { name = "harp-device" }, @@ -337,6 +347,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ + { name = "codespell", specifier = ">=2.3.0" }, { name = "harp-benchmarks", editable = "src/packages/harp-benchmarks" }, { name = "harp-data", editable = "src/packages/harp-data" }, { name = "harp-device", editable = "src/packages/harp-device" }, From 3ac77d59f848ddab9641240caf8b479c88dfdca1 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 17 Jul 2026 11:40:15 -0700 Subject: [PATCH 257/267] Fix site name --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index c420ad3..f4718b3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,4 +1,4 @@ -site_name: pyharp +site_name: harp repo_url: https://github.com/harp-tech/pyharp repo_name: "harp-tech/pyharp" copyright: Copyright (c) harp-tech and Contributors From 1f3e8e64ca8431a33019c31f93f370712ec308bf Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 17 Jul 2026 11:44:48 -0700 Subject: [PATCH 258/267] Fix type hints --- .../harp-data/src/harp/data/_reader.py | 6 +++--- .../src/harp/protocol/_payload.py | 20 ++++++++++--------- .../src/harp/protocol/_payload_converters.py | 4 ++-- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/packages/harp-data/src/harp/data/_reader.py b/src/packages/harp-data/src/harp/data/_reader.py index aea02a5..b068df4 100644 --- a/src/packages/harp-data/src/harp/data/_reader.py +++ b/src/packages/harp-data/src/harp/data/_reader.py @@ -15,9 +15,9 @@ def _read_bytes(source: Source) -> bytes: if isinstance(source, (bytes, bytearray, memoryview)): return bytes(source) - if hasattr(source, "read"): # open binary file / stream - return source.read() - return Path(source).read_bytes() + if isinstance(source, (str, Path)): + return Path(source).read_bytes() + return source.read() # open binary file / stream _DEFAULT_COLUMN_NAME = "value" diff --git a/src/packages/harp-protocol/src/harp/protocol/_payload.py b/src/packages/harp-protocol/src/harp/protocol/_payload.py index 5e06359..9960532 100644 --- a/src/packages/harp-protocol/src/harp/protocol/_payload.py +++ b/src/packages/harp-protocol/src/harp/protocol/_payload.py @@ -348,6 +348,7 @@ def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: def _to_batch(self) -> "_BitMaskBatch[F]": """Returns the metadata for the corresponding batch type""" + assert self._mask is not None # _bind_slot ensures every masked field has a mask return _BitMaskBatch( self._mask, self._enum, @@ -359,6 +360,7 @@ def _columns( self, arr: "NDArray[Any]", name: "str | None", *, decode_enums: bool, demux_bit_masks: bool ) -> "list[Column]": """A single raw-integer column, or (``demux_bit_masks``) one bool column per flag member.""" + assert self._mask is not None # _bind_slot ensures every masked field has a mask raw = arr[self._slot] & self._mask if not demux_bit_masks: return [Column(name, raw)] @@ -597,7 +599,7 @@ def _build_struct_dtype( else: itemsize = max(slot.byte_offset + slot.dtype.itemsize for slot in slots.values()) _validate_no_overlap(cls, slots, itemsize) - return np.dtype( + return np.dtype( # ty: ignore[no-matching-overload] { "names": list(slots), "formats": [slot.dtype for slot in slots.values()], @@ -686,7 +688,7 @@ def __init__(self, *args: object, **kwargs: object) -> None: self._arr = arr @classmethod - def _mro_descriptor(cls, name: str) -> object | None: + def _mro_descriptor(cls, name: str) -> "Field[Any] | GroupMask[Any] | BitMask[Any] | None": for klass in cls.__mro__: if name in klass.__dict__: return klass.__dict__[name] @@ -812,10 +814,9 @@ def to_columns( cols: list[Column] = [] for f in self._repr_fields: desc = scalar_cls._mro_descriptor(f) + assert desc is not None cols.extend( - desc._columns( # ty: ignore[possibly-unbound-attribute] - arr, f, decode_enums=decode_enums, demux_bit_masks=demux_bit_masks - ) + desc._columns(arr, f, decode_enums=decode_enums, demux_bit_masks=demux_bit_masks) ) return cols @@ -950,7 +951,7 @@ def __init_subclass__( f"multi-field payloads." ) cls._root = True - super().__init_subclass__(**kwargs) + super().__init_subclass__(**kwargs) # ty: ignore[invalid-argument-type] return # Raw scalar slot required, unless a Batch twin / array concrete supplies dtype. if scalar_dtype is None and "_batch_of" not in kwargs and "dtype" not in cls.__dict__: @@ -962,7 +963,7 @@ def __init_subclass__( if scalar_dtype is not None: cls.dtype = np.dtype(scalar_dtype) cls._repr_fields = () - super().__init_subclass__(**kwargs) + super().__init_subclass__(**kwargs) # ty: ignore[invalid-argument-type] def __init__(self, value: object = _MISSING_INIT, /, **kwargs: object) -> None: # type: ignore[override] if type(self)._root: @@ -979,7 +980,7 @@ def __init__(self, value: object = _MISSING_INIT, /, **kwargs: object) -> None: raise TypeError(f"{type(self).__name__}() requires a value") if kwargs: raise TypeError(f"{type(self).__name__}() got unexpected kwargs: {sorted(kwargs)}") - self._arr = np.asarray(value, dtype=self.dtype) # ty: ignore[invalid-assignment] + self._arr = np.asarray(value, dtype=self.dtype) @classmethod def unwrap(cls, arr: "np.ndarray") -> Any: @@ -1004,7 +1005,8 @@ def to_columns( if type(self)._root: arr = np.atleast_1d(self._arr) root = type(self)._scalar_cls._mro_descriptor(self._VALUE_FIELD) - return root._columns( # ty: ignore[possibly-unbound-attribute] + assert root is not None + return root._columns( arr, None, decode_enums=decode_enums, demux_bit_masks=demux_bit_masks ) arr = np.atleast_1d(self._arr) diff --git a/src/packages/harp-protocol/src/harp/protocol/_payload_converters.py b/src/packages/harp-protocol/src/harp/protocol/_payload_converters.py index 545e2c8..809fac5 100644 --- a/src/packages/harp-protocol/src/harp/protocol/_payload_converters.py +++ b/src/packages/harp-protocol/src/harp/protocol/_payload_converters.py @@ -171,7 +171,7 @@ def __init__(self, length: int, encoding: str = "ascii") -> None: self.dtype = np.dtype((np.uint8, (length,))) def decode_scalar(self, view: np.generic) -> str: - return bytes(view).rstrip(b"\x00").decode(self._encoding) # ty: ignore[invalid-argument-type] + return bytes(view).rstrip(b"\x00").decode(self._encoding) _VIEW_CASTABLE_ENCODINGS = frozenset({"ascii", "latin1", "latin-1", "iso-8859-1"}) @@ -181,7 +181,7 @@ def decode_batch(self, view: NDArray[np.generic]) -> Any: [bytes(row).rstrip(b"\x00").decode(self._encoding) for row in view], dtype=object, ) - return view.reshape(-1, self._length).view(f"S{self._length}").reshape(-1).astype(str) + return view.reshape(-1, self._length).view(f"S{self._length}").reshape(-1).astype(str) # ty: ignore[no-matching-overload] def encode_into(self, view: NDArray[np.generic], value: str) -> None: encoded = value.encode(self._encoding)[: self._length] From 0da63ba21aa9ef7292cf04cc37ff89302489a62a Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:12:37 -0700 Subject: [PATCH 259/267] Add benchmark to CICD --- .github/workflows/pyharp.yml | 428 +++++++++++------- .python-version | 3 + src/packages/harp-benchmarks/pyproject.toml | 1 + .../src/harp/benchmarks/benchmark.py | 89 +++- .../src/harp/benchmarks/compare.py | 223 +++++++++ .../src/harp/benchmarks/generate.py | 31 +- 6 files changed, 592 insertions(+), 183 deletions(-) create mode 100644 .python-version create mode 100644 src/packages/harp-benchmarks/src/harp/benchmarks/compare.py diff --git a/.github/workflows/pyharp.yml b/.github/workflows/pyharp.yml index 8e240cb..d75b84c 100644 --- a/.github/workflows/pyharp.yml +++ b/.github/workflows/pyharp.yml @@ -1,170 +1,268 @@ name: pyharp test suite on: - pull_request: - push: - branches: - - master - release: - types: [published] - workflow_dispatch: + pull_request: + push: + branches: + - master + release: + types: [published] + workflow_dispatch: jobs: - # ---- Tests ---- - tests: - name: Python ${{ matrix.python-version }} on ${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ["3.11", "3.12", "3.13"] - fail-fast: false - steps: - - uses: actions/checkout@v7 - - - uses: astral-sh/setup-uv@v8.3.2 - with: - enable-cache: true - - - name: Install python dependencies - run: uv sync --group dev - - - name: Run codespell - run: uv run codespell - - - name: Run ruff format - run: uv run ruff format --check - - - name: Run ruff check - run: uv run ruff check - - - name: Run ty - run: uv run ty check - - - name: Run pytest - run: uv run pytest --cov harp - - - name: Build - run: uv build --all-packages - - prepare-release: - runs-on: ubuntu-latest - name: Set version from release tag - needs: tests - if: github.event_name == 'release' && github.event.action == 'published' - outputs: - version: ${{ steps.get_version.outputs.version }} - - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - ref: master - - - uses: astral-sh/setup-uv@v8.3.2 - with: - enable-cache: true - - - name: Extract version from tag - id: get_version - shell: bash - run: | - version=$(echo "${{ github.event.release.tag_name }}" | sed 's/^v//') - echo "version=$version" >> $GITHUB_OUTPUT - echo "Setting version to: $version" - - - name: Validate version format - run: uv version ${{ steps.get_version.outputs.version }} --dry-run - - - name: Set version across all workspace packages - shell: bash - run: | - version="${{ steps.get_version.outputs.version }}" - # Discover every workspace member (root + src/packages/*) by its declared - # package name so all versions move in lock-step with the release tag. - mapfile -t pkgs < <(grep -h '^name = ' pyproject.toml src/packages/*/pyproject.toml | sed -E 's/^name = "(.*)"/\1/') - for pkg in "${pkgs[@]}"; do - echo "Setting $pkg -> $version" - uv version --package "$pkg" "$version" - done - - - name: Build - run: uv build --all-packages - - - name: Remove internal-only packages from dist - shell: bash - run: rm -f dist/harp_benchmarks-* - - - name: Upload wheels as artifact - uses: actions/upload-artifact@v7 - with: - name: dist - path: dist/ - - # This step seems a bit complicated, but it allows for idempotent commits, - # so that if the release tag is moved to a new commit, - # the version will be updated and committed again. - - name: Commit version changes - shell: bash - run: | - git config --global user.name "github-actions[bot]" - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git add . - git diff --cached --quiet && echo "No changes to commit" && exit 0 - git commit -m "Set version ${{ steps.get_version.outputs.version }} [skip ci]" - git push origin master - # Move the release tag to point to this new commit. - git tag -fa "${{ github.event.release.tag_name }}" -m "${{ github.event.release.tag_name }}" - git push origin "${{ github.event.release.tag_name }}" --force - - # ---- Publish to PyPI ---- - publish-to-pypi: - runs-on: ubuntu-latest - name: Publish to PyPI - needs: prepare-release - steps: - - name: Download wheels artifact - uses: actions/download-artifact@v8 - with: - name: dist - path: dist/ - - - uses: astral-sh/setup-uv@v8.3.2 - with: - enable-cache: true - - - name: Publish to PyPI - run: uv publish --token ${{ secrets.PYPI_TOKEN }} - - - name: Upload wheels to GitHub release - uses: softprops/action-gh-release@v3 - with: - files: dist/* - - # ---- Docs ---- - build-docs: - name: Build and deploy documentation to GitHub Pages - runs-on: ubuntu-latest - needs: prepare-release - if: github.event_name == 'release' && !github.event.release.prerelease - steps: - - name: Checkout - uses: actions/checkout@v7 - with: - ref: master - - - name: Install uv - uses: astral-sh/setup-uv@v8.3.2 - with: - enable-cache: true - - - name: Install dependencies - run: uv sync --group docs - - - name: Configure Git user - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - - name: Build & Deploy docs - run: uv run mkdocs gh-deploy --force + # ---- Tests ---- + tests: + name: Python ${{ matrix.python-version }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.11", "3.12", "3.13"] + fail-fast: false + steps: + - uses: actions/checkout@v7 + + - uses: astral-sh/setup-uv@v8.3.2 + with: + enable-cache: true + + - name: Install python dependencies + run: uv sync --group dev --python ${{ matrix.python-version }} + + - name: Run codespell + run: uv run codespell + + - name: Run ruff format + run: uv run ruff format --check + + - name: Run ruff check + run: uv run ruff check + + - name: Run ty + run: uv run ty check + + - name: Run pytest + run: uv run pytest --cov harp + + - name: Build + run: uv build --all-packages + + # ---- Benchmarks (base vs PR, informational only — never blocks merge) ---- + benchmarks: + name: Benchmark base vs PR on ${{ matrix.os }} + if: github.event_name == 'pull_request' + needs: tests + permissions: + contents: read + pull-requests: write + strategy: + matrix: + os: [ubuntu-latest] # Only run benchmarks on Linux for now... + fail-fast: false + runs-on: ${{ matrix.os }} + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - uses: astral-sh/setup-uv@v8.3.2 + with: + enable-cache: true + + - name: Set up shared paths + run: | + echo "BENCH_DIR=${RUNNER_TEMP//\\//}/harp-bench" >> "$GITHUB_ENV" + echo "OUT_DIR=${RUNNER_TEMP//\\//}/harp-bench-out" >> "$GITHUB_ENV" + + - name: Benchmark PR (generates the shared corpus) + run: | + mkdir -p "$OUT_DIR" + uv sync --group dev + uv run harp-benchmark --entries 200000 --runs 5 \ + --dir "$BENCH_DIR/data" --json "$OUT_DIR/pr.json" + + - name: Benchmark base (reuses the shared corpus) + continue-on-error: true + run: | + git fetch --no-tags --depth=1 origin "$GITHUB_BASE_REF" + BASE_WT="${RUNNER_TEMP//\\//}/harp-base" + git worktree add --detach "$BASE_WT" "origin/$GITHUB_BASE_REF" + cd "$BASE_WT" + uv sync --group dev + uv run harp-benchmark --entries 200000 --runs 5 \ + --dir "$BENCH_DIR/data" --json "$OUT_DIR/base.json" + + - name: Render comparison + run: | + if [ -f "$OUT_DIR/base.json" ]; then + uv run harp-benchmark-compare \ + --base "$OUT_DIR/base.json" --pr "$OUT_DIR/pr.json" \ + --label "${{ matrix.os }}" --threshold 10 \ + --out "$OUT_DIR/comment.md" + else + { + echo "" + echo "" + echo "## 🏁 Parsing benchmark — base vs PR (${{ matrix.os }})" + echo "" + echo "_Baseline unavailable: base branch \`$GITHUB_BASE_REF\` does not yet emit" + echo "benchmark JSON. Comparison will appear once this tooling lands on the base branch._" + } > "$OUT_DIR/comment.md" + fi + cat "$OUT_DIR/comment.md" + + - name: Upload benchmark artifacts + uses: actions/upload-artifact@v7 + with: + name: benchmark-${{ matrix.os }} + path: | + ${{ env.OUT_DIR }}/pr.json + ${{ env.OUT_DIR }}/base.json + ${{ env.OUT_DIR }}/comment.md + if-no-files-found: warn + + - name: Post sticky PR comment + if: github.event.pull_request.head.repo.full_name == github.repository + continue-on-error: true + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const marker = ``; + const body = fs.readFileSync(`${process.env.OUT_DIR}/comment.md`, 'utf8'); + const { owner, repo } = context.repo; + const issue_number = context.payload.pull_request.number; + const comments = await github.paginate(github.rest.issues.listComments, { + owner, repo, issue_number, + }); + const existing = comments.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }); + } else { + await github.rest.issues.createComment({ owner, repo, issue_number, body }); + } + + prepare-release: + runs-on: ubuntu-latest + name: Set version from release tag + needs: tests + if: github.event_name == 'release' && github.event.action == 'published' + outputs: + version: ${{ steps.get_version.outputs.version }} + + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + ref: master + + - uses: astral-sh/setup-uv@v8.3.2 + with: + enable-cache: true + + - name: Extract version from tag + id: get_version + shell: bash + run: | + version=$(echo "${{ github.event.release.tag_name }}" | sed 's/^v//') + echo "version=$version" >> $GITHUB_OUTPUT + echo "Setting version to: $version" + + - name: Validate version format + run: uv version ${{ steps.get_version.outputs.version }} --dry-run + + - name: Set version across all workspace packages + shell: bash + run: | + version="${{ steps.get_version.outputs.version }}" + # Discover every workspace member (root + src/packages/*) by its declared + # package name so all versions move in lock-step with the release tag. + mapfile -t pkgs < <(grep -h '^name = ' pyproject.toml src/packages/*/pyproject.toml | sed -E 's/^name = "(.*)"/\1/') + for pkg in "${pkgs[@]}"; do + echo "Setting $pkg -> $version" + uv version --package "$pkg" "$version" + done + + - name: Build + run: uv build --all-packages + + - name: Remove internal-only packages from dist + shell: bash + run: rm -f dist/harp_benchmarks-* + + - name: Upload wheels as artifact + uses: actions/upload-artifact@v7 + with: + name: dist + path: dist/ + + # This step seems a bit complicated, but it allows for idempotent commits, + # so that if the release tag is moved to a new commit, + # the version will be updated and committed again. + - name: Commit version changes + shell: bash + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git add . + git diff --cached --quiet && echo "No changes to commit" && exit 0 + git commit -m "Set version ${{ steps.get_version.outputs.version }} [skip ci]" + git push origin master + # Move the release tag to point to this new commit. + git tag -fa "${{ github.event.release.tag_name }}" -m "${{ github.event.release.tag_name }}" + git push origin "${{ github.event.release.tag_name }}" --force + + # ---- Publish to PyPI ---- + publish-to-pypi: + runs-on: ubuntu-latest + name: Publish to PyPI + needs: prepare-release + steps: + - name: Download wheels artifact + uses: actions/download-artifact@v8 + with: + name: dist + path: dist/ + + - uses: astral-sh/setup-uv@v8.3.2 + with: + enable-cache: true + + - name: Publish to PyPI + run: uv publish --token ${{ secrets.PYPI_TOKEN }} + + - name: Upload wheels to GitHub release + uses: softprops/action-gh-release@v3 + with: + files: dist/* + + # ---- Docs ---- + build-docs: + name: Build and deploy documentation to GitHub Pages + runs-on: ubuntu-latest + needs: prepare-release + if: github.event_name == 'release' && !github.event.release.prerelease + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + ref: master + + - name: Install uv + uses: astral-sh/setup-uv@v8.3.2 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --group docs + + - name: Configure Git user + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Build & Deploy docs + run: uv run mkdocs gh-deploy --force diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..42dc33b --- /dev/null +++ b/.python-version @@ -0,0 +1,3 @@ +3.13 +3.12 +3.11 diff --git a/src/packages/harp-benchmarks/pyproject.toml b/src/packages/harp-benchmarks/pyproject.toml index 9caa8f0..17f9c37 100644 --- a/src/packages/harp-benchmarks/pyproject.toml +++ b/src/packages/harp-benchmarks/pyproject.toml @@ -17,6 +17,7 @@ dependencies = [ [project.scripts] harp-benchmark = "harp.benchmarks.benchmark:main" harp-benchmark-generate = "harp.benchmarks.generate:main" +harp-benchmark-compare = "harp.benchmarks.compare:main" [build-system] requires = ["uv_build>=0.9.5"] diff --git a/src/packages/harp-benchmarks/src/harp/benchmarks/benchmark.py b/src/packages/harp-benchmarks/src/harp/benchmarks/benchmark.py index 19a36dc..4de0635 100644 --- a/src/packages/harp-benchmarks/src/harp/benchmarks/benchmark.py +++ b/src/packages/harp-benchmarks/src/harp/benchmarks/benchmark.py @@ -1,4 +1,5 @@ import argparse +import json import platform import sys from dataclasses import dataclass @@ -9,7 +10,6 @@ import numpy as np import pandas as pd - from harp.benchmarks._registers import ( BENCHMARK_REGISTERS, DATA_DIR, @@ -250,6 +250,63 @@ def _table( return "\n".join(lines) +# Bump when the JSON layout below changes incompatibly; ``compare`` checks it. +BENCHMARK_SCHEMA_VERSION = 1 + +# Measured operations, in report order — shared vocabulary with ``compare.py``. +METRIC_LABELS: dict[str, str] = { + "bulk_preread": "parse_bulk (pre-read)", + "bulk_reread": "parse_bulk (re-read)", + "cols": "to_columns (decode)", + "df_preread": "parse_to_dataframe (pre-read)", + "df_reread": "parse_to_dataframe (re-read)", +} + + +def _stats_to_dict(s: TimingStats) -> dict: + return { + "min": s.min, + "mean": s.mean, + "max": s.max, + "stdev": s.stdev, + "mframes_per_s": s.mframes_per_s, + "mib_per_s": s.mib_per_s, + } + + +def results_to_payload(results: list[RegisterResult], *, runs: int, entries: int) -> dict: + """Machine-readable results (see ``--json``), keyed by register + metric for diffing.""" + return { + "schema_version": BENCHMARK_SCHEMA_VERSION, + "environment": { + "platform": platform.platform(), + "python": sys.version.split()[0], + "numpy": np.__version__, + "pandas": pd.__version__, + "runs": runs, + "entries": entries, + }, + "registers": [ + { + "name": r.name, + "address": r.address, + "frames": r.frames, + "stride": r.stride, + "payload_bytes": r.payload_bytes, + "file_bytes": r.file_bytes, + "metrics": { + "bulk_preread": _stats_to_dict(r.bulk_preread), + "bulk_reread": _stats_to_dict(r.bulk_reread), + "cols": _stats_to_dict(r.cols), + "df_preread": _stats_to_dict(r.df_preread), + "df_reread": _stats_to_dict(r.df_reread), + }, + } + for r in results + ], + } + + def _select(only): selected = BENCHMARK_REGISTERS if only: @@ -261,15 +318,15 @@ def _select(only): return selected -def _prepare_corpora(selected, *, entries: int, force: bool) -> None: +def _prepare_corpora(selected, *, entries: int, force: bool, data_dir: Path) -> None: """Generate any missing corpora (cache honored unless ``force``), printing progress.""" - DATA_DIR.mkdir(parents=True, exist_ok=True) + data_dir.mkdir(parents=True, exist_ok=True) n = len(selected) mode = "rebuilding" if force else "cache honored" - print(f"Preparing {n} corpus file(s) in {DATA_DIR} ({mode}, {entries:,} frames each):") + print(f"Preparing {n} corpus file(s) in {data_dir} ({mode}, {entries:,} frames each):") for i, reg in enumerate(selected, 1): print(f" [{i}/{n}] {reg.name:<24s} ", end="", flush=True) - _, generated = ensure_corpus(reg, entries, force=force) + _, generated = ensure_corpus(reg, entries, force=force, data_dir=data_dir) print("generated" if generated else "cached") print() @@ -301,7 +358,19 @@ def main() -> None: parser.add_argument( "--only", nargs="+", metavar="NAME", help="restrict to these register names (default: all)" ) + parser.add_argument( + "--dir", + type=Path, + default=DATA_DIR, + help=f"directory containing/receiving corpus files (default: {DATA_DIR})", + ) parser.add_argument("--report", type=Path, default=REPORT_PATH) + parser.add_argument( + "--json", + type=Path, + default=None, + help="also write machine-readable results (for base-vs-PR comparison) to this path", + ) parser.add_argument( "--head", action="store_true", @@ -312,13 +381,13 @@ def main() -> None: selected = _select(args.only) # Ensure the data exists before benchmarking (honor the cache unless --force). - _prepare_corpora(selected, entries=args.entries, force=args.force) + _prepare_corpora(selected, entries=args.entries, force=args.force, data_dir=args.dir) n = len(selected) results: list[RegisterResult] = [] print(f"Benchmarking {n} register(s), {args.runs} runs each:") for i, reg in enumerate(selected, 1): - path = DATA_DIR / reg.filename + path = args.dir / reg.filename print(f" [{i}/{n}] {reg.name:<24s} ", end="", flush=True) res = benchmark_register(reg, path, runs=args.runs) results.append(res) @@ -337,6 +406,12 @@ def main() -> None: args.report.write_text(report, encoding="utf-8") print(f"\nReport written to {args.report}") + if args.json is not None: + args.json.parent.mkdir(parents=True, exist_ok=True) + payload = results_to_payload(results, runs=args.runs, entries=args.entries) + args.json.write_text(json.dumps(payload, indent=2), encoding="utf-8") + print(f"JSON written to {args.json}") + if __name__ == "__main__": main() diff --git a/src/packages/harp-benchmarks/src/harp/benchmarks/compare.py b/src/packages/harp-benchmarks/src/harp/benchmarks/compare.py new file mode 100644 index 0000000..511ad0d --- /dev/null +++ b/src/packages/harp-benchmarks/src/harp/benchmarks/compare.py @@ -0,0 +1,223 @@ +"""Render a Markdown comparison of two ``harp-benchmark --json`` result files. + +Used by CI to post a base-vs-PR parsing-speed diff as a pull-request comment. The +output is pure Markdown; a hidden ```` marker lets the +workflow keep a single sticky comment per runner. This is a comparison only — the +command always exits 0 (merges are never gated on it). +""" + +import argparse +import json +import sys +from pathlib import Path + +# Keep in sync with harp.benchmarks.benchmark.METRIC_LABELS / BENCHMARK_SCHEMA_VERSION. +METRIC_LABELS: dict[str, str] = { + "bulk_preread": "parse_bulk (pre-read)", + "bulk_reread": "parse_bulk (re-read)", + "cols": "to_columns (decode)", + "df_preread": "parse_to_dataframe (pre-read)", + "df_reread": "parse_to_dataframe (re-read)", +} +DEFAULT_THRESHOLD = 10.0 # percent; |Δ| in min-time beyond this is called out + + +def _load(path: Path) -> dict: + return json.loads(path.read_text(encoding="utf-8")) + + +def _index(payload: dict) -> dict[str, dict]: + """Map register name -> its record (metrics, frames, ...).""" + return {r["name"]: r for r in payload.get("registers", [])} + + +def _ms(seconds: float) -> float: + return seconds * 1e3 + + +def _pct(base: float, pr: float) -> float | None: + """Percent change base->pr; positive means slower (a regression). None if base is 0.""" + if base == 0: + return None + return (pr - base) / base * 100.0 + + +def _fmt_pct(p: float | None) -> str: + return "—" if p is None else f"{p:+.1f}%" + + +def _flag(p: float | None, threshold: float) -> str: + if p is None: + return "" + if p >= threshold: + return " ⚠️" + if p <= -threshold: + return " ✅" + return "" + + +def _environment_table(base_env: dict, pr_env: dict) -> tuple[list[str], list[str]]: + """Return (markdown lines, warnings) for the base-vs-PR environment block.""" + keys = ["platform", "python", "numpy", "pandas", "entries", "runs"] + lines = ["| Key | base | PR |", "| --- | --- | --- |"] + warnings: list[str] = [] + for k in keys: + b, p = base_env.get(k, "?"), pr_env.get(k, "?") + same = b == p + lines.append(f"| {k} | {b} | {p} |") + # A different interpreter/numpy/pandas makes absolute timings incomparable. + if not same and k in {"python", "numpy", "pandas", "platform"}: + warnings.append(f"`{k}` differs (base `{b}` vs PR `{p}`)") + return lines, warnings + + +def _collect_deltas( + base_regs: dict[str, dict], pr_regs: dict[str, dict] +) -> list[tuple[str, str, float, float, float]]: + """(register, metric, base_min_ms, pr_min_ms, delta_pct) for every shared register+metric.""" + rows: list[tuple[str, str, float, float, float]] = [] + for name in base_regs.keys() & pr_regs.keys(): + b_metrics = base_regs[name]["metrics"] + p_metrics = pr_regs[name]["metrics"] + for metric in METRIC_LABELS: + if metric not in b_metrics or metric not in p_metrics: + continue + b_min, p_min = b_metrics[metric]["min"], p_metrics[metric]["min"] + pct = _pct(b_min, p_min) + if pct is None: + continue + rows.append((name, metric, _ms(b_min), _ms(p_min), pct)) + return rows + + +def _summary_section( + deltas: list[tuple[str, str, float, float, float]], threshold: float +) -> list[str]: + """Notable min-time movers, worst regressions first.""" + notable = sorted( + (d for d in deltas if abs(d[4]) >= threshold), key=lambda d: d[4], reverse=True + ) + lines = [f"### Summary (min-time changes ≥ ±{threshold:g}%)", ""] + if not deltas: + lines.append("_No overlapping registers to compare._") + return lines + if not notable: + lines.append(f"No min-time changes beyond ±{threshold:g}%. 🎉") + return lines + lines.append("| Register | Metric | base (ms) | PR (ms) | Δ min |") + lines.append("| --- | --- | ---: | ---: | ---: |") + for name, metric, b_ms, p_ms, pct in notable: + lines.append( + f"| {name} | {METRIC_LABELS[metric]} | {b_ms:.3f} | {p_ms:.3f} " + f"| {_fmt_pct(pct)}{_flag(pct, threshold)} |" + ) + return lines + + +def _detail_tables( + base_regs: dict[str, dict], pr_regs: dict[str, dict], threshold: float +) -> list[str]: + """One collapsible table per measured operation: min/mean/max/std, base vs PR.""" + lines: list[str] = [] + names = sorted(base_regs.keys() & pr_regs.keys()) + for metric, label in METRIC_LABELS.items(): + lines.append(f"
{label} — per register (ms)") + lines.append("") + lines.append( + "| Register | mean base | mean PR | Δ mean | min base | min PR | Δ min " + "| max base | max PR | std base | std PR |" + ) + lines.append( + "| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |" + ) + for name in names: + b = base_regs[name]["metrics"].get(metric) + p = pr_regs[name]["metrics"].get(metric) + if b is None or p is None: + continue + d_mean = _pct(b["mean"], p["mean"]) + d_min = _pct(b["min"], p["min"]) + lines.append( + f"| {name} " + f"| {_ms(b['mean']):.3f} | {_ms(p['mean']):.3f} | {_fmt_pct(d_mean)} " + f"| {_ms(b['min']):.3f} | {_ms(p['min']):.3f} | {_fmt_pct(d_min)}{_flag(d_min, threshold)} " + f"| {_ms(b['max']):.3f} | {_ms(p['max']):.3f} " + f"| {_ms(b['stdev']):.3f} | {_ms(p['stdev']):.3f} |" + ) + lines.append("") + lines.append("
") + lines.append("") + return lines + + +def render(base: dict, pr: dict, *, label: str, threshold: float) -> str: + marker = f"" if label else "" + title = f"## 🏁 Parsing benchmark — base vs PR{f' ({label})' if label else ''}" + lines: list[str] = [marker, "", title, ""] + + env_lines, warnings = _environment_table(base.get("environment", {}), pr.get("environment", {})) + if warnings: + lines.append( + "> ⚠️ Environment differs, so absolute timings are not directly comparable: " + + "; ".join(warnings) + + "." + ) + lines.append("") + lines.append("
Environment") + lines.append("") + lines.extend(env_lines) + lines.append("") + lines.append("
") + lines.append("") + + base_regs, pr_regs = _index(base), _index(pr) + added = sorted(pr_regs.keys() - base_regs.keys()) + removed = sorted(base_regs.keys() - pr_regs.keys()) + if added: + lines.append(f"> ➕ Only in PR: {', '.join(added)}") + lines.append("") + if removed: + lines.append(f"> ➖ Only in base: {', '.join(removed)}") + lines.append("") + + deltas = _collect_deltas(base_regs, pr_regs) + lines.extend(_summary_section(deltas, threshold)) + lines.append("") + lines.append( + "_Δ is `(PR − base) / base`; positive = slower (regression). " + "**min** is the headline (most stable on shared CI runners); mean/max/std shown for context._" + ) + lines.append("") + lines.extend(_detail_tables(base_regs, pr_regs, threshold)) + return "\n".join(lines).rstrip() + "\n" + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--base", type=Path, required=True, help="baseline results JSON") + parser.add_argument("--pr", type=Path, required=True, help="PR results JSON") + parser.add_argument( + "--out", type=Path, default=None, help="write Markdown here (default: stdout)" + ) + parser.add_argument( + "--label", default="", help="runner label, e.g. the OS (sticky-comment key)" + ) + parser.add_argument( + "--threshold", + type=float, + default=DEFAULT_THRESHOLD, + help=f"|Δ min-time| %% to flag (default: {DEFAULT_THRESHOLD:g})", + ) + args = parser.parse_args() + + md = render(_load(args.base), _load(args.pr), label=args.label, threshold=args.threshold) + if args.out is not None: + args.out.parent.mkdir(parents=True, exist_ok=True) + args.out.write_text(md, encoding="utf-8") + print(f"Comparison written to {args.out}") + else: + sys.stdout.write(md) + + +if __name__ == "__main__": + main() diff --git a/src/packages/harp-benchmarks/src/harp/benchmarks/generate.py b/src/packages/harp-benchmarks/src/harp/benchmarks/generate.py index 17787de..64c62ad 100644 --- a/src/packages/harp-benchmarks/src/harp/benchmarks/generate.py +++ b/src/packages/harp-benchmarks/src/harp/benchmarks/generate.py @@ -1,31 +1,34 @@ import argparse import sys +from pathlib import Path from harp.benchmarks._registers import BENCHMARK_REGISTERS, DATA_DIR, BenchmarkedRegister _TIMESTAMP = 42 -def corpus_path(reg: BenchmarkedRegister): - """Path to ``reg``'s corpus file under the artifacts data directory.""" - return DATA_DIR / reg.filename +def corpus_path(reg: BenchmarkedRegister, data_dir: Path = DATA_DIR): + """Path to ``reg``'s corpus file under ``data_dir``.""" + return data_dir / reg.filename def _frame_timestamp(reg: BenchmarkedRegister) -> int | None: return _TIMESTAMP if reg.timestamped else None -def generate_one(reg: BenchmarkedRegister, entries: int) -> tuple[str, int, int]: +def generate_one( + reg: BenchmarkedRegister, entries: int, data_dir: Path = DATA_DIR +) -> tuple[str, int, int]: """Write ``entries`` frames for ``reg``. Returns (path, frame_size, file_size).""" frame = reg.register.format(reg.value, timestamp=_frame_timestamp(reg)) - path = corpus_path(reg) + path = corpus_path(reg, data_dir) path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(frame * entries) return str(path), len(frame), path.stat().st_size def ensure_corpus( - reg: BenchmarkedRegister, entries: int, *, force: bool = False + reg: BenchmarkedRegister, entries: int, *, force: bool = False, data_dir: Path = DATA_DIR ) -> tuple[object, bool]: """Generate ``reg``'s corpus unless a matching cached file already exists. @@ -33,12 +36,12 @@ def ensure_corpus( exactly (frame_size * entries); a stale file (different entry count) is rebuilt. Returns (path, generated). """ - path = corpus_path(reg) + path = corpus_path(reg, data_dir) if path.exists() and not force: frame_size = len(reg.register.format(reg.value, timestamp=_frame_timestamp(reg))) if path.stat().st_size == frame_size * entries: return path, False - generate_one(reg, entries) + generate_one(reg, entries, data_dir) return path, True @@ -73,16 +76,22 @@ def main() -> None: metavar="NAME", help="restrict generation to these register names (default: all)", ) + parser.add_argument( + "--dir", + type=Path, + default=DATA_DIR, + help=f"directory to write corpus files into (default: {DATA_DIR})", + ) _use_utf8_console() args = parser.parse_args() selected = _select(args.only) - DATA_DIR.mkdir(parents=True, exist_ok=True) + args.dir.mkdir(parents=True, exist_ok=True) - print(f"Generating {args.entries:,} frames/register into {DATA_DIR}\n") + print(f"Generating {args.entries:,} frames/register into {args.dir}\n") total_bytes = 0 for reg in selected: - _, frame_size, file_size = generate_one(reg, args.entries) + _, frame_size, file_size = generate_one(reg, args.entries, args.dir) total_bytes += file_size print( f" {reg.name:<24s} addr={reg.address:<4d} " From 4329e1d90644bf7e0693e08fadf6abe2816cd0e3 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 3 Jul 2026 19:43:49 -0700 Subject: [PATCH 260/267] Add observer/observable pattern to device --- .../harp-device/src/harp/device/__init__.py | 4 +- .../harp-device/src/harp/device/_device.py | 192 ++++++++++++++++-- 2 files changed, 183 insertions(+), 13 deletions(-) diff --git a/src/packages/harp-device/src/harp/device/__init__.py b/src/packages/harp-device/src/harp/device/__init__.py index e61d1f3..ac81527 100644 --- a/src/packages/harp-device/src/harp/device/__init__.py +++ b/src/packages/harp-device/src/harp/device/__init__.py @@ -1,4 +1,4 @@ -from ._device import Device +from ._device import Device, EventHandler, Subscription from ._framer import HarpFramer from ._registers import ( AssemblyVersion, @@ -30,6 +30,8 @@ __all__ = [ "Device", + "EventHandler", + "Subscription", "HarpFramer", "ITransport", "TransportError", diff --git a/src/packages/harp-device/src/harp/device/_device.py b/src/packages/harp-device/src/harp/device/_device.py index 09a5382..b6dc1b6 100644 --- a/src/packages/harp-device/src/harp/device/_device.py +++ b/src/packages/harp-device/src/harp/device/_device.py @@ -1,7 +1,9 @@ """Transport-agnostic Harp device base class.""" +from collections.abc import Callable, Iterable from typing import Any, ClassVar, Self, TypeVar +import logging import queue import threading @@ -17,6 +19,53 @@ P = TypeVar("P") +_logger = logging.getLogger(__name__) + +#: A callback receiving a typed, parsed event for a specific register. +EventHandler = Callable[[ParsedHarpMessage[P]], None] + +#: Message types a subscription reacts to, as a single type or an iterable. +MessageTypeFilter = MessageType | Iterable[MessageType] + +#: Default filter for :meth:`Device.subscribe`: unsolicited events only. +_DEFAULT_MESSAGE_TYPES: frozenset[MessageType] = frozenset({MessageType.Event}) + + +def _normalize_message_types(message_types: MessageTypeFilter) -> frozenset[MessageType]: + if isinstance(message_types, MessageType): + return frozenset({message_types}) + return frozenset(message_types) + + +class Subscription: + """Handle returned by :meth:`Device.subscribe`. Cancel with + :meth:`unsubscribe`, or use as a context manager to auto-cancel on exit.""" + + def __init__( + self, + device: "Device", + address: int | None, + handler: Callable[[Any], None], + message_types: frozenset[MessageType], + ) -> None: + self._device = device + self._address = address # None => catch-all + self._handler = handler + self._message_types = message_types + self._active = True + + def unsubscribe(self) -> None: + """Stop delivering events to this subscription. Idempotent.""" + if self._active: + self._device._remove_subscription(self) + self._active = False + + def __enter__(self) -> "Subscription": + return self + + def __exit__(self, *args: object) -> None: + self.unsubscribe() + class Device: """Harp device protocol logic (framing, request/reply, register access) @@ -41,6 +90,14 @@ def __init__(self, transport: ITransport, *, raise_on_error: bool = True) -> Non self._running = False self._thread: threading.Thread | None = None + # Event subscriptions, delivered off the reader thread (see _event_loop). + self._subscriptions: dict[int, list[Subscription]] = {} + self._registers: dict[int, type[RegisterBase[Any]]] = {} + self._catch_all: list[Subscription] = [] + self._sub_lock = threading.Lock() + self._event_queue: queue.SimpleQueue[HarpMessage | None] = queue.SimpleQueue() + self._event_thread: threading.Thread | None = None + # ------------------------------------------------------------------ # Lifecycle # ------------------------------------------------------------------ @@ -49,6 +106,10 @@ def open(self) -> Self: """Open the transport, start the reader thread and validate identity.""" self._transport.open() self._running = True + self._event_thread = threading.Thread( + target=self._event_loop, daemon=True, name=f"{type(self).__name__}-events" + ) + self._event_thread.start() self._thread = threading.Thread( target=self._read_loop, daemon=True, name=f"{type(self).__name__}-reader" ) @@ -77,6 +138,10 @@ def close(self) -> None: if self._thread is not None: self._thread.join(timeout=2.0) self._thread = None + if self._event_thread is not None: + self._event_queue.put(None) # wake the loop so it can exit + self._event_thread.join(timeout=2.0) + self._event_thread = None self._transport.close() def __enter__(self) -> Self: @@ -119,11 +184,115 @@ def write( return ParsedHarpMessage.from_message(msg, register.parse(msg)) # ------------------------------------------------------------------ - # Internals + # Events # ------------------------------------------------------------------ - def _on_event(self, msg: HarpMessage) -> None: - """Handle an Event message from the reader thread. Default: discard.""" + def subscribe( + self, + register: type[RegisterBase[P]], + handler: EventHandler[P], + *, + message_types: MessageTypeFilter = MessageType.Event, + ) -> Subscription: + """Call ``handler`` with a typed, parsed :class:`ParsedHarpMessage` each + time the device emits a message for ``register``. + + By default only unsolicited ``Event`` messages are delivered. Pass + ``message_types`` (a :class:`MessageType` or an iterable of them) to also + observe ``Read``/``Write`` replies, e.g. + ``message_types=(MessageType.Event, MessageType.Write)``. + + Handlers run on a single dedicated event thread, shared by *all* + subscribers, so they may block or call back into :meth:`read`/:meth:`write` + without deadlocking the reader or delaying synchronous requests. However, + because that thread is shared, handlers are invoked **sequentially, in + subscription order, one message at a time**: a slow handler delays every + other subscriber and backs up later messages. Keep handlers quick, and + offload heavy work to your own thread or queue. + + Returns a :class:`Subscription`; call :meth:`Subscription.unsubscribe` to + stop. + """ + sub = Subscription( + self, register.address, handler, _normalize_message_types(message_types) + ) + with self._sub_lock: + self._subscriptions.setdefault(register.address, []).append(sub) + self._registers[register.address] = register + return sub + + def subscribe_all( + self, + handler: Callable[[HarpMessage], None], + *, + message_types: MessageTypeFilter = MessageType.Event, + ) -> Subscription: + """Call ``handler`` with the raw :class:`HarpMessage` for every message, + regardless of address, whose type is in ``message_types`` (default: + ``Event`` only). Pass more types for a full-traffic firehose, e.g. a + logger. See :meth:`subscribe` for threading and cancellation semantics.""" + sub = Subscription(self, None, handler, _normalize_message_types(message_types)) + with self._sub_lock: + self._catch_all.append(sub) + return sub + + def _remove_subscription(self, sub: "Subscription") -> None: + with self._sub_lock: + if sub._address is None: + try: + self._catch_all.remove(sub) + except ValueError: + pass + else: + subs = self._subscriptions.get(sub._address) + if subs is not None: + try: + subs.remove(sub) + except ValueError: + pass + if not subs: + del self._subscriptions[sub._address] + self._registers.pop(sub._address, None) + + def _event_loop(self) -> None: + while True: + msg = self._event_queue.get() + if msg is None: # shutdown sentinel + break + self._deliver_event(msg) + + def _deliver_event(self, msg: HarpMessage) -> None: + with self._sub_lock: + subs = list(self._subscriptions.get(msg.address, ())) + register = self._registers.get(msg.address) + catch_all = list(self._catch_all) + + matching = [s for s in subs if msg.message_type in s._message_types] + if matching and register is not None: + try: + parsed = ParsedHarpMessage.from_message(msg, register.parse(msg)) + except Exception: + _logger.exception( + "Failed to parse %r for address 0x%02x", msg.message_type, msg.address + ) + else: + for sub in matching: + self._safe_call(sub._handler, parsed) + + for sub in catch_all: + if msg.message_type in sub._message_types: + self._safe_call(sub._handler, msg) + + @staticmethod + def _safe_call(handler: Callable[[Any], None], arg: Any) -> None: + try: + handler(arg) + except Exception: + _logger.exception("Event handler %r raised", handler) + + # ------------------------------------------------------------------ + # Internals + # ------------------------------------------------------------------ def _read_loop(self) -> None: while self._running: @@ -141,20 +310,19 @@ def _read_loop(self) -> None: self._dispatch(msg) def _dispatch(self, msg: HarpMessage) -> None: - if msg.message_type in (MessageType.Read, MessageType.Write): - with self._pending_lock: - q = self._pending.get(msg.address) - if q is not None: - q.put(msg) - elif msg.message_type == MessageType.Event: - self._on_event(msg) - else: - # Unknown message type + # Fast path: correlate replies to a pending synchronous request. This is + # O(1) and non-blocking, so it never stalls behind a slow subscriber. + # Events are unsolicited and never correlate. + if msg.message_type != MessageType.Event: with self._pending_lock: q = self._pending.get(msg.address) if q is not None: q.put(msg) + # In parallel: fan *every* message out to the observation stream, where + # subscribers filter by message type on the dedicated event thread. + self._event_queue.put(msg) + def _request(self, address: int, frame: bytes) -> HarpMessage: q: queue.SimpleQueue = queue.SimpleQueue() with self._pending_lock: From 19e1f1c9cf30f3d088e4fb0af25ffacaedb54c46 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 3 Jul 2026 19:44:41 -0700 Subject: [PATCH 261/267] Clear device subscriptions on close to prevent stale handlers --- src/packages/harp-device/src/harp/device/_device.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/packages/harp-device/src/harp/device/_device.py b/src/packages/harp-device/src/harp/device/_device.py index b6dc1b6..c2ba454 100644 --- a/src/packages/harp-device/src/harp/device/_device.py +++ b/src/packages/harp-device/src/harp/device/_device.py @@ -143,6 +143,10 @@ def close(self) -> None: self._event_thread.join(timeout=2.0) self._event_thread = None self._transport.close() + with self._sub_lock: + self._subscriptions.clear() + self._registers.clear() + self._catch_all.clear() def __enter__(self) -> Self: if not self._running: @@ -213,9 +217,7 @@ def subscribe( Returns a :class:`Subscription`; call :meth:`Subscription.unsubscribe` to stop. """ - sub = Subscription( - self, register.address, handler, _normalize_message_types(message_types) - ) + sub = Subscription(self, register.address, handler, _normalize_message_types(message_types)) with self._sub_lock: self._subscriptions.setdefault(register.address, []).append(sub) self._registers[register.address] = register From e729912875b1d155084e742cc721726a6b76871e Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:24:51 -0700 Subject: [PATCH 262/267] Add examples for event handling --- .../subscribing_to_events.md | 17 +++++++ .../subscribing_to_events.py | 45 +++++++++++++++++++ mkdocs.yml | 1 + .../harp-device/src/harp/device/_device.py | 6 +-- 4 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 docs/examples/subscribing_to_events/subscribing_to_events.md create mode 100644 docs/examples/subscribing_to_events/subscribing_to_events.py diff --git a/docs/examples/subscribing_to_events/subscribing_to_events.md b/docs/examples/subscribing_to_events/subscribing_to_events.md new file mode 100644 index 0000000..796c4b2 --- /dev/null +++ b/docs/examples/subscribing_to_events/subscribing_to_events.md @@ -0,0 +1,17 @@ +# Subscribing to Events + +This example demonstrates how to react to messages pushed by the device — e.g. unsolicited `Event` messages — without polling, using two subscription styles: + +- `device.subscribe(register, handler)` — the handler receives a typed, parsed `ParsedHarpMessage` for a single register. +- `device.subscribe_all(handler)` — a catch-all handler that receives the raw `HarpMessage` for every register. + +Handlers run on a dedicated event thread, so they never block `read()`/`write()`. Both methods return a `Subscription`; call `.unsubscribe()` (or use it as a context manager) to stop receiving events. + +!!! warning + Don't forget to change the `SERIAL_PORT` to the one that corresponds to your device! The `SERIAL_PORT` must be denoted as `/dev/ttyUSBx` in Linux and `COMx` in Windows, where `x` is the number of the serial port. + + +```python +[](./subscribing_to_events.py) +``` + diff --git a/docs/examples/subscribing_to_events/subscribing_to_events.py b/docs/examples/subscribing_to_events/subscribing_to_events.py new file mode 100644 index 0000000..c649514 --- /dev/null +++ b/docs/examples/subscribing_to_events/subscribing_to_events.py @@ -0,0 +1,45 @@ +from harp.device import ( + REGISTER_MAP, + Device, + OperationControl, + OperationControlPayload, + OperationMode, + TimestampSeconds, +) +from harp.protocol import HarpMessage, ParsedHarpMessage +from harp.serial import open_serial_device + +SERIAL_PORT = "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port) + + +def print_timestamp(msg: ParsedHarpMessage[float]) -> None: + print(f"[timestamp] {msg.timestamp:.6f} {msg.parsed}") + + +def print_any_event(msg: HarpMessage) -> None: + register = REGISTER_MAP.get(msg.address, None) + value = register.parse(msg) if register is not None else msg.payload.hex() + print(f"[{msg.address}] {msg.timestamp:.6f} {msg.message_type.name:<5s} {value}") + + +with open_serial_device(Device, port=SERIAL_PORT) as device: + # Subscribe to a single, typed register: the handler receives a parsed payload. + timestamp_subscription = device.subscribe(TimestampSeconds, print_timestamp) + + # Subscribe to every register at once: the handler receives the raw message. + device.subscribe_all(print_any_event) + + device.write( + OperationControl, + OperationControlPayload( + operation_mode=OperationMode.ACTIVE, + dump_registers=True, + heartbeat=True, + mute_replies=False, + operation_led=True, + visual_indicators=True, + ), + ) + + input("Listening for events. Press Enter to stop.\n") + timestamp_subscription.unsubscribe() diff --git a/mkdocs.yml b/mkdocs.yml index f4718b3..68ce965 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -75,6 +75,7 @@ nav: - Getting Device Info: examples/get_info/get_info.md - Read and Write from Registers: examples/read_and_write_from_registers/read_and_write_from_registers.md - Reading Data into a DataFrame: examples/read_data_to_dataframe/read_data_to_dataframe.md + - Subscribing to Events: examples/subscribing_to_events/subscribing_to_events.md - API: - Protocol: api/protocol.md - Serial: api/serial.md diff --git a/src/packages/harp-device/src/harp/device/_device.py b/src/packages/harp-device/src/harp/device/_device.py index c2ba454..69adfd7 100644 --- a/src/packages/harp-device/src/harp/device/_device.py +++ b/src/packages/harp-device/src/harp/device/_device.py @@ -313,16 +313,14 @@ def _read_loop(self) -> None: def _dispatch(self, msg: HarpMessage) -> None: # Fast path: correlate replies to a pending synchronous request. This is - # O(1) and non-blocking, so it never stalls behind a slow subscriber. - # Events are unsolicited and never correlate. + # O(1) and non-blocking, so it should never stall behind a slow subscriber. + # Events are unsolicited and never correlate to requests if msg.message_type != MessageType.Event: with self._pending_lock: q = self._pending.get(msg.address) if q is not None: q.put(msg) - # In parallel: fan *every* message out to the observation stream, where - # subscribers filter by message type on the dedicated event thread. self._event_queue.put(msg) def _request(self, address: int, frame: bytes) -> HarpMessage: From 47f9c5351412e227dceb7685d8e9c004adfa22b5 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:51:24 -0700 Subject: [PATCH 263/267] Remove benchmarks from CICD --- .github/workflows/pyharp.yml | 98 -------- src/packages/harp-benchmarks/pyproject.toml | 1 - .../src/harp/benchmarks/benchmark.py | 70 ------ .../src/harp/benchmarks/compare.py | 223 ------------------ 4 files changed, 392 deletions(-) delete mode 100644 src/packages/harp-benchmarks/src/harp/benchmarks/compare.py diff --git a/.github/workflows/pyharp.yml b/.github/workflows/pyharp.yml index d75b84c..ee11652 100644 --- a/.github/workflows/pyharp.yml +++ b/.github/workflows/pyharp.yml @@ -47,104 +47,6 @@ jobs: - name: Build run: uv build --all-packages - # ---- Benchmarks (base vs PR, informational only — never blocks merge) ---- - benchmarks: - name: Benchmark base vs PR on ${{ matrix.os }} - if: github.event_name == 'pull_request' - needs: tests - permissions: - contents: read - pull-requests: write - strategy: - matrix: - os: [ubuntu-latest] # Only run benchmarks on Linux for now... - fail-fast: false - runs-on: ${{ matrix.os }} - defaults: - run: - shell: bash - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - uses: astral-sh/setup-uv@v8.3.2 - with: - enable-cache: true - - - name: Set up shared paths - run: | - echo "BENCH_DIR=${RUNNER_TEMP//\\//}/harp-bench" >> "$GITHUB_ENV" - echo "OUT_DIR=${RUNNER_TEMP//\\//}/harp-bench-out" >> "$GITHUB_ENV" - - - name: Benchmark PR (generates the shared corpus) - run: | - mkdir -p "$OUT_DIR" - uv sync --group dev - uv run harp-benchmark --entries 200000 --runs 5 \ - --dir "$BENCH_DIR/data" --json "$OUT_DIR/pr.json" - - - name: Benchmark base (reuses the shared corpus) - continue-on-error: true - run: | - git fetch --no-tags --depth=1 origin "$GITHUB_BASE_REF" - BASE_WT="${RUNNER_TEMP//\\//}/harp-base" - git worktree add --detach "$BASE_WT" "origin/$GITHUB_BASE_REF" - cd "$BASE_WT" - uv sync --group dev - uv run harp-benchmark --entries 200000 --runs 5 \ - --dir "$BENCH_DIR/data" --json "$OUT_DIR/base.json" - - - name: Render comparison - run: | - if [ -f "$OUT_DIR/base.json" ]; then - uv run harp-benchmark-compare \ - --base "$OUT_DIR/base.json" --pr "$OUT_DIR/pr.json" \ - --label "${{ matrix.os }}" --threshold 10 \ - --out "$OUT_DIR/comment.md" - else - { - echo "" - echo "" - echo "## 🏁 Parsing benchmark — base vs PR (${{ matrix.os }})" - echo "" - echo "_Baseline unavailable: base branch \`$GITHUB_BASE_REF\` does not yet emit" - echo "benchmark JSON. Comparison will appear once this tooling lands on the base branch._" - } > "$OUT_DIR/comment.md" - fi - cat "$OUT_DIR/comment.md" - - - name: Upload benchmark artifacts - uses: actions/upload-artifact@v7 - with: - name: benchmark-${{ matrix.os }} - path: | - ${{ env.OUT_DIR }}/pr.json - ${{ env.OUT_DIR }}/base.json - ${{ env.OUT_DIR }}/comment.md - if-no-files-found: warn - - - name: Post sticky PR comment - if: github.event.pull_request.head.repo.full_name == github.repository - continue-on-error: true - uses: actions/github-script@v7 - with: - script: | - const fs = require('fs'); - const marker = ``; - const body = fs.readFileSync(`${process.env.OUT_DIR}/comment.md`, 'utf8'); - const { owner, repo } = context.repo; - const issue_number = context.payload.pull_request.number; - const comments = await github.paginate(github.rest.issues.listComments, { - owner, repo, issue_number, - }); - const existing = comments.find(c => c.body.includes(marker)); - if (existing) { - await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }); - } else { - await github.rest.issues.createComment({ owner, repo, issue_number, body }); - } - prepare-release: runs-on: ubuntu-latest name: Set version from release tag diff --git a/src/packages/harp-benchmarks/pyproject.toml b/src/packages/harp-benchmarks/pyproject.toml index 17f9c37..9caa8f0 100644 --- a/src/packages/harp-benchmarks/pyproject.toml +++ b/src/packages/harp-benchmarks/pyproject.toml @@ -17,7 +17,6 @@ dependencies = [ [project.scripts] harp-benchmark = "harp.benchmarks.benchmark:main" harp-benchmark-generate = "harp.benchmarks.generate:main" -harp-benchmark-compare = "harp.benchmarks.compare:main" [build-system] requires = ["uv_build>=0.9.5"] diff --git a/src/packages/harp-benchmarks/src/harp/benchmarks/benchmark.py b/src/packages/harp-benchmarks/src/harp/benchmarks/benchmark.py index 4de0635..2d75563 100644 --- a/src/packages/harp-benchmarks/src/harp/benchmarks/benchmark.py +++ b/src/packages/harp-benchmarks/src/harp/benchmarks/benchmark.py @@ -1,5 +1,4 @@ import argparse -import json import platform import sys from dataclasses import dataclass @@ -250,63 +249,6 @@ def _table( return "\n".join(lines) -# Bump when the JSON layout below changes incompatibly; ``compare`` checks it. -BENCHMARK_SCHEMA_VERSION = 1 - -# Measured operations, in report order — shared vocabulary with ``compare.py``. -METRIC_LABELS: dict[str, str] = { - "bulk_preread": "parse_bulk (pre-read)", - "bulk_reread": "parse_bulk (re-read)", - "cols": "to_columns (decode)", - "df_preread": "parse_to_dataframe (pre-read)", - "df_reread": "parse_to_dataframe (re-read)", -} - - -def _stats_to_dict(s: TimingStats) -> dict: - return { - "min": s.min, - "mean": s.mean, - "max": s.max, - "stdev": s.stdev, - "mframes_per_s": s.mframes_per_s, - "mib_per_s": s.mib_per_s, - } - - -def results_to_payload(results: list[RegisterResult], *, runs: int, entries: int) -> dict: - """Machine-readable results (see ``--json``), keyed by register + metric for diffing.""" - return { - "schema_version": BENCHMARK_SCHEMA_VERSION, - "environment": { - "platform": platform.platform(), - "python": sys.version.split()[0], - "numpy": np.__version__, - "pandas": pd.__version__, - "runs": runs, - "entries": entries, - }, - "registers": [ - { - "name": r.name, - "address": r.address, - "frames": r.frames, - "stride": r.stride, - "payload_bytes": r.payload_bytes, - "file_bytes": r.file_bytes, - "metrics": { - "bulk_preread": _stats_to_dict(r.bulk_preread), - "bulk_reread": _stats_to_dict(r.bulk_reread), - "cols": _stats_to_dict(r.cols), - "df_preread": _stats_to_dict(r.df_preread), - "df_reread": _stats_to_dict(r.df_reread), - }, - } - for r in results - ], - } - - def _select(only): selected = BENCHMARK_REGISTERS if only: @@ -365,12 +307,6 @@ def main() -> None: help=f"directory containing/receiving corpus files (default: {DATA_DIR})", ) parser.add_argument("--report", type=Path, default=REPORT_PATH) - parser.add_argument( - "--json", - type=Path, - default=None, - help="also write machine-readable results (for base-vs-PR comparison) to this path", - ) parser.add_argument( "--head", action="store_true", @@ -406,12 +342,6 @@ def main() -> None: args.report.write_text(report, encoding="utf-8") print(f"\nReport written to {args.report}") - if args.json is not None: - args.json.parent.mkdir(parents=True, exist_ok=True) - payload = results_to_payload(results, runs=args.runs, entries=args.entries) - args.json.write_text(json.dumps(payload, indent=2), encoding="utf-8") - print(f"JSON written to {args.json}") - if __name__ == "__main__": main() diff --git a/src/packages/harp-benchmarks/src/harp/benchmarks/compare.py b/src/packages/harp-benchmarks/src/harp/benchmarks/compare.py deleted file mode 100644 index 511ad0d..0000000 --- a/src/packages/harp-benchmarks/src/harp/benchmarks/compare.py +++ /dev/null @@ -1,223 +0,0 @@ -"""Render a Markdown comparison of two ``harp-benchmark --json`` result files. - -Used by CI to post a base-vs-PR parsing-speed diff as a pull-request comment. The -output is pure Markdown; a hidden ```` marker lets the -workflow keep a single sticky comment per runner. This is a comparison only — the -command always exits 0 (merges are never gated on it). -""" - -import argparse -import json -import sys -from pathlib import Path - -# Keep in sync with harp.benchmarks.benchmark.METRIC_LABELS / BENCHMARK_SCHEMA_VERSION. -METRIC_LABELS: dict[str, str] = { - "bulk_preread": "parse_bulk (pre-read)", - "bulk_reread": "parse_bulk (re-read)", - "cols": "to_columns (decode)", - "df_preread": "parse_to_dataframe (pre-read)", - "df_reread": "parse_to_dataframe (re-read)", -} -DEFAULT_THRESHOLD = 10.0 # percent; |Δ| in min-time beyond this is called out - - -def _load(path: Path) -> dict: - return json.loads(path.read_text(encoding="utf-8")) - - -def _index(payload: dict) -> dict[str, dict]: - """Map register name -> its record (metrics, frames, ...).""" - return {r["name"]: r for r in payload.get("registers", [])} - - -def _ms(seconds: float) -> float: - return seconds * 1e3 - - -def _pct(base: float, pr: float) -> float | None: - """Percent change base->pr; positive means slower (a regression). None if base is 0.""" - if base == 0: - return None - return (pr - base) / base * 100.0 - - -def _fmt_pct(p: float | None) -> str: - return "—" if p is None else f"{p:+.1f}%" - - -def _flag(p: float | None, threshold: float) -> str: - if p is None: - return "" - if p >= threshold: - return " ⚠️" - if p <= -threshold: - return " ✅" - return "" - - -def _environment_table(base_env: dict, pr_env: dict) -> tuple[list[str], list[str]]: - """Return (markdown lines, warnings) for the base-vs-PR environment block.""" - keys = ["platform", "python", "numpy", "pandas", "entries", "runs"] - lines = ["| Key | base | PR |", "| --- | --- | --- |"] - warnings: list[str] = [] - for k in keys: - b, p = base_env.get(k, "?"), pr_env.get(k, "?") - same = b == p - lines.append(f"| {k} | {b} | {p} |") - # A different interpreter/numpy/pandas makes absolute timings incomparable. - if not same and k in {"python", "numpy", "pandas", "platform"}: - warnings.append(f"`{k}` differs (base `{b}` vs PR `{p}`)") - return lines, warnings - - -def _collect_deltas( - base_regs: dict[str, dict], pr_regs: dict[str, dict] -) -> list[tuple[str, str, float, float, float]]: - """(register, metric, base_min_ms, pr_min_ms, delta_pct) for every shared register+metric.""" - rows: list[tuple[str, str, float, float, float]] = [] - for name in base_regs.keys() & pr_regs.keys(): - b_metrics = base_regs[name]["metrics"] - p_metrics = pr_regs[name]["metrics"] - for metric in METRIC_LABELS: - if metric not in b_metrics or metric not in p_metrics: - continue - b_min, p_min = b_metrics[metric]["min"], p_metrics[metric]["min"] - pct = _pct(b_min, p_min) - if pct is None: - continue - rows.append((name, metric, _ms(b_min), _ms(p_min), pct)) - return rows - - -def _summary_section( - deltas: list[tuple[str, str, float, float, float]], threshold: float -) -> list[str]: - """Notable min-time movers, worst regressions first.""" - notable = sorted( - (d for d in deltas if abs(d[4]) >= threshold), key=lambda d: d[4], reverse=True - ) - lines = [f"### Summary (min-time changes ≥ ±{threshold:g}%)", ""] - if not deltas: - lines.append("_No overlapping registers to compare._") - return lines - if not notable: - lines.append(f"No min-time changes beyond ±{threshold:g}%. 🎉") - return lines - lines.append("| Register | Metric | base (ms) | PR (ms) | Δ min |") - lines.append("| --- | --- | ---: | ---: | ---: |") - for name, metric, b_ms, p_ms, pct in notable: - lines.append( - f"| {name} | {METRIC_LABELS[metric]} | {b_ms:.3f} | {p_ms:.3f} " - f"| {_fmt_pct(pct)}{_flag(pct, threshold)} |" - ) - return lines - - -def _detail_tables( - base_regs: dict[str, dict], pr_regs: dict[str, dict], threshold: float -) -> list[str]: - """One collapsible table per measured operation: min/mean/max/std, base vs PR.""" - lines: list[str] = [] - names = sorted(base_regs.keys() & pr_regs.keys()) - for metric, label in METRIC_LABELS.items(): - lines.append(f"
{label} — per register (ms)") - lines.append("") - lines.append( - "| Register | mean base | mean PR | Δ mean | min base | min PR | Δ min " - "| max base | max PR | std base | std PR |" - ) - lines.append( - "| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |" - ) - for name in names: - b = base_regs[name]["metrics"].get(metric) - p = pr_regs[name]["metrics"].get(metric) - if b is None or p is None: - continue - d_mean = _pct(b["mean"], p["mean"]) - d_min = _pct(b["min"], p["min"]) - lines.append( - f"| {name} " - f"| {_ms(b['mean']):.3f} | {_ms(p['mean']):.3f} | {_fmt_pct(d_mean)} " - f"| {_ms(b['min']):.3f} | {_ms(p['min']):.3f} | {_fmt_pct(d_min)}{_flag(d_min, threshold)} " - f"| {_ms(b['max']):.3f} | {_ms(p['max']):.3f} " - f"| {_ms(b['stdev']):.3f} | {_ms(p['stdev']):.3f} |" - ) - lines.append("") - lines.append("
") - lines.append("") - return lines - - -def render(base: dict, pr: dict, *, label: str, threshold: float) -> str: - marker = f"" if label else "" - title = f"## 🏁 Parsing benchmark — base vs PR{f' ({label})' if label else ''}" - lines: list[str] = [marker, "", title, ""] - - env_lines, warnings = _environment_table(base.get("environment", {}), pr.get("environment", {})) - if warnings: - lines.append( - "> ⚠️ Environment differs, so absolute timings are not directly comparable: " - + "; ".join(warnings) - + "." - ) - lines.append("") - lines.append("
Environment") - lines.append("") - lines.extend(env_lines) - lines.append("") - lines.append("
") - lines.append("") - - base_regs, pr_regs = _index(base), _index(pr) - added = sorted(pr_regs.keys() - base_regs.keys()) - removed = sorted(base_regs.keys() - pr_regs.keys()) - if added: - lines.append(f"> ➕ Only in PR: {', '.join(added)}") - lines.append("") - if removed: - lines.append(f"> ➖ Only in base: {', '.join(removed)}") - lines.append("") - - deltas = _collect_deltas(base_regs, pr_regs) - lines.extend(_summary_section(deltas, threshold)) - lines.append("") - lines.append( - "_Δ is `(PR − base) / base`; positive = slower (regression). " - "**min** is the headline (most stable on shared CI runners); mean/max/std shown for context._" - ) - lines.append("") - lines.extend(_detail_tables(base_regs, pr_regs, threshold)) - return "\n".join(lines).rstrip() + "\n" - - -def main() -> None: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--base", type=Path, required=True, help="baseline results JSON") - parser.add_argument("--pr", type=Path, required=True, help="PR results JSON") - parser.add_argument( - "--out", type=Path, default=None, help="write Markdown here (default: stdout)" - ) - parser.add_argument( - "--label", default="", help="runner label, e.g. the OS (sticky-comment key)" - ) - parser.add_argument( - "--threshold", - type=float, - default=DEFAULT_THRESHOLD, - help=f"|Δ min-time| %% to flag (default: {DEFAULT_THRESHOLD:g})", - ) - args = parser.parse_args() - - md = render(_load(args.base), _load(args.pr), label=args.label, threshold=args.threshold) - if args.out is not None: - args.out.parent.mkdir(parents=True, exist_ok=True) - args.out.write_text(md, encoding="utf-8") - print(f"Comparison written to {args.out}") - else: - sys.stdout.write(md) - - -if __name__ == "__main__": - main() From de793a0eb6b1fc6e3ec3712e231b918310db53ba Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 17 Jul 2026 14:06:09 -0700 Subject: [PATCH 264/267] Add logo --- README.md | 65 +++++++++++++++++++++++++++++++++++++++++++- docs/assets/logo.svg | 64 +++++++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 3 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 docs/assets/logo.svg diff --git a/README.md b/README.md index 02b0a30..12358dd 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,8 @@ -# pyharp +

+ Harp logo +

+ +# harp This project includes four main packages: @@ -14,6 +18,65 @@ This project includes four main packages: - **harp-data**: Parses register binary dumps into pandas DataFrames. See [Data API Documentation](https://harp-tech.org/pyharp/api/data) for more information. +## Installation + +All packages are published to PyPI. The `harp` package is a metadata package with no code of +its own — it just depends on the four packages above, so it's the easiest way to get everything: + +```sh +pip install harp +``` + +```sh +uv add harp +``` + +If you only need part of the stack (e.g. you're parsing offline data dumps and don't need serial +I/O), install just the packages you need — each one only pulls in what it actually depends on: + +| Package | Provides | Depends on | +| --- | --- | --- | +| `harp-protocol` | Core protocol types: registers, messages, payload parsing | — | +| `harp-device` | Transport-agnostic `Device` class, common register map | `harp-protocol` | +| `harp-serial` | Serial (COM/tty) transport for `Device` | `harp-protocol`, `harp-device` | +| `harp-data` | Parse register binary dumps into pandas DataFrames | `harp-protocol` | + +```sh +pip install harp-protocol +pip install harp-device +pip install harp-serial +pip install harp-data +``` + +`harp-benchmarks` (under `src/packages/`) is internal-only and is never published to PyPI. + +## Contributing + +harp is a [uv workspace](https://docs.astral.sh/uv/concepts/workspaces/): every package under +`src/packages/` is its own distribution, plus the root `harp` metadata package. Contributions are +welcome — please open an issue or PR. + +Clone the repo and install everything (all workspace packages, editable, plus test/lint tooling) +with the `dev` dependency group: + +```sh +uv sync --group dev +``` + +Before opening a PR, run the same checks CI runs: + +```sh +uv run ruff format --check # formatting +uv run ruff check # lint +uv run ty check # type checking +uv run codespell # spelling +uv run pytest --cov harp # tests +``` + +Adding a new package? Drop it under `src/packages//` with its own `pyproject.toml`, add it +to `[tool.uv.sources]` in the root `pyproject.toml`, and (if it should ship as part of `harp`) add +it to the root package's `dependencies` too. + ## Building the documentation Install the docs dependency group and run mkdocs through uv: diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg new file mode 100644 index 0000000..ed0e7bb --- /dev/null +++ b/docs/assets/logo.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + diff --git a/mkdocs.yml b/mkdocs.yml index 68ce965..48b3d6b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -40,6 +40,7 @@ theme: name: material icon: repo: fontawesome/brands/github + logo: assets/logo.svg favicon: assets/favicon.png features: - content.tooltips From 9a71d40c2df6de8e1dbdcebabb525eb67bdbb10e Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 17 Jul 2026 14:11:45 -0700 Subject: [PATCH 265/267] Use relative path --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 12358dd..9b35717 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- Harp logo + Harp logo

# harp From af09088fbee425c1913a7c1ddb1c9ac39665e0e0 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Sat, 25 Jul 2026 00:13:54 +0100 Subject: [PATCH 266/267] Switch to setuptools-scm and trusted publishing All six workspace packages build with the setuptools backend and take their version from setuptools-scm, each pointing at the repository root so one tag versions them in lockstep. The setuptools floor is 77, the first release supporting the PEP 639 license expression that harp and harp-protocol already declare. No version file is written, so runtime lookups go through importlib.metadata. The release job no longer sets versions in the tree, commits back to master, or force-moves the release tag. It passes the release tag to setuptools-scm through SETUPTOOLS_SCM_PRETEND_VERSION, fails if any distribution falls back to 0.0.0, and publishes through PyPI trusted publishing in a pypi environment rather than a token secret. The docs job deploys from the tag and fetches full history for git-authors attribution. The workflow is renamed to harp.yml after the distribution it builds. pyright replaces ty in the dev group and in CI, in standard mode, with three ty suppressions in harp-protocol translated to pyright and three more dropped. --- .github/workflows/{pyharp.yml => harp.yml} | 78 +++++++------------ pyproject.toml | 48 ++++++++---- src/packages/harp-benchmarks/pyproject.toml | 15 ++-- src/packages/harp-data/pyproject.toml | 15 ++-- src/packages/harp-device/pyproject.toml | 15 ++-- src/packages/harp-protocol/pyproject.toml | 15 ++-- .../src/harp/protocol/_payload.py | 10 +-- .../src/harp/protocol/_payload_converters.py | 2 +- src/packages/harp-serial/pyproject.toml | 15 ++-- uv.lock | 57 ++++++-------- 10 files changed, 135 insertions(+), 135 deletions(-) rename .github/workflows/{pyharp.yml => harp.yml} (56%) diff --git a/.github/workflows/pyharp.yml b/.github/workflows/harp.yml similarity index 56% rename from .github/workflows/pyharp.yml rename to .github/workflows/harp.yml index ee11652..d436ad0 100644 --- a/.github/workflows/pyharp.yml +++ b/.github/workflows/harp.yml @@ -1,4 +1,4 @@ -name: pyharp test suite +name: harp on: pull_request: @@ -38,8 +38,8 @@ jobs: - name: Run ruff check run: uv run ruff check - - name: Run ty - run: uv run ty check + - name: Run pyright + run: uv run pyright - name: Run pytest run: uv run pytest --cov harp @@ -47,49 +47,33 @@ jobs: - name: Build run: uv build --all-packages - prepare-release: + # ---- Release build ---- + build-release: runs-on: ubuntu-latest - name: Set version from release tag + name: Build release distributions needs: tests if: github.event_name == 'release' && github.event.action == 'published' - outputs: - version: ${{ steps.get_version.outputs.version }} steps: - uses: actions/checkout@v7 - with: - fetch-depth: 0 - ref: master - uses: astral-sh/setup-uv@v8.3.2 with: enable-cache: true - - name: Extract version from tag - id: get_version - shell: bash - run: | - version=$(echo "${{ github.event.release.tag_name }}" | sed 's/^v//') - echo "version=$version" >> $GITHUB_OUTPUT - echo "Setting version to: $version" - - - name: Validate version format - run: uv version ${{ steps.get_version.outputs.version }} --dry-run + - name: Build + env: + SETUPTOOLS_SCM_PRETEND_VERSION: ${{ github.event.release.tag_name }} + run: uv build --all-packages - - name: Set version across all workspace packages + - name: Verify setuptools-scm applied the release tag shell: bash run: | - version="${{ steps.get_version.outputs.version }}" - # Discover every workspace member (root + src/packages/*) by its declared - # package name so all versions move in lock-step with the release tag. - mapfile -t pkgs < <(grep -h '^name = ' pyproject.toml src/packages/*/pyproject.toml | sed -E 's/^name = "(.*)"/\1/') - for pkg in "${pkgs[@]}"; do - echo "Setting $pkg -> $version" - uv version --package "$pkg" "$version" - done - - - name: Build - run: uv build --all-packages + ls -1 dist/ + if ls dist/*-0.0.0.tar.gz >/dev/null 2>&1; then + echo "::error::Built version 0.0.0, so setuptools-scm did not apply the tag. Check that every pyproject.toml still declares a tool.setuptools_scm table." + exit 1 + fi - name: Remove internal-only packages from dist shell: bash @@ -101,27 +85,17 @@ jobs: name: dist path: dist/ - # This step seems a bit complicated, but it allows for idempotent commits, - # so that if the release tag is moved to a new commit, - # the version will be updated and committed again. - - name: Commit version changes - shell: bash - run: | - git config --global user.name "github-actions[bot]" - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git add . - git diff --cached --quiet && echo "No changes to commit" && exit 0 - git commit -m "Set version ${{ steps.get_version.outputs.version }} [skip ci]" - git push origin master - # Move the release tag to point to this new commit. - git tag -fa "${{ github.event.release.tag_name }}" -m "${{ github.event.release.tag_name }}" - git push origin "${{ github.event.release.tag_name }}" --force - # ---- Publish to PyPI ---- publish-to-pypi: runs-on: ubuntu-latest name: Publish to PyPI - needs: prepare-release + needs: build-release + environment: pypi + permissions: + # uv publish exchanges this token with PyPI trusted publishing. + id-token: write + # action-gh-release attaches the distributions to the release. + contents: write steps: - name: Download wheels artifact uses: actions/download-artifact@v8 @@ -134,7 +108,7 @@ jobs: enable-cache: true - name: Publish to PyPI - run: uv publish --token ${{ secrets.PYPI_TOKEN }} + run: uv publish - name: Upload wheels to GitHub release uses: softprops/action-gh-release@v3 @@ -145,13 +119,13 @@ jobs: build-docs: name: Build and deploy documentation to GitHub Pages runs-on: ubuntu-latest - needs: prepare-release + needs: build-release if: github.event_name == 'release' && !github.event.release.prerelease steps: - name: Checkout uses: actions/checkout@v7 with: - ref: master + fetch-depth: 0 - name: Install uv uses: astral-sh/setup-uv@v8.3.2 diff --git a/pyproject.toml b/pyproject.toml index f01826f..c7d9169 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "harp" -version = "0.0.0" +dynamic = ["version"] description = "Library for data acquisition and control of devices implementing the Harp protocol." authors = [{ name = "harp-tech", email = "contact@harp-tech.org" }] license = "MIT" @@ -44,13 +44,14 @@ Documentation = "https://harp-tech.org/pyharp/" Changelog = "https://github.com/harp-tech/pyharp/releases" [build-system] -requires = ["uv_build>=0.9.5"] -build-backend = "uv_build" +requires = ["setuptools>=77", "setuptools-scm>=8"] +build-backend = "setuptools.build_meta" -[tool.uv.build-backend] -module-name = "harp" -module-root = "src" -namespace = true +[tool.setuptools_scm] + +[tool.setuptools.packages.find] +where = ["src"] +include = ["harp*"] [tool.uv.sources] harp-protocol = { workspace = true } @@ -77,10 +78,10 @@ docs = [ dev = [ "codespell>=2.3.0", + "pyright>=1.1.411", "pytest>=8.3.5", "pytest-cov>=6.1.1", "ruff>=0.11.0", - "ty>=0.0.0", "harp-protocol", "harp-device", "harp-serial", @@ -109,11 +110,9 @@ python_files = [ line-length = 100 target-version = "py311" -[tool.ty.environment] -python-version = "3.11" -extra-paths = ["tests"] - -[tool.ty.src] +[tool.pyright] +pythonVersion = "3.11" +typeCheckingMode = "standard" include = [ "src/packages/harp-protocol/src", "src/packages/harp-device/src", @@ -121,5 +120,26 @@ include = [ "src/packages/harp-data/src", ] exclude = [ + "**/node_modules", + "**/__pycache__", + "**/.*", + ".venv", "tests", -] \ No newline at end of file +] +venvPath = "." +venv = ".venv" +reportImportCycles = "error" +reportUnusedImport = "error" +reportUnusedClass = "error" +reportUnusedFunction = "error" +reportUnusedVariable = "error" +reportDuplicateImport = "error" +reportWildcardImportFromLibrary = "error" +reportCallInDefaultInitializer = "error" +reportUnnecessaryIsInstance = "error" +reportUnnecessaryCast = "error" +reportUnnecessaryContains = "error" +reportAssertAlwaysTrue = "error" +reportSelfClsParameterName = "error" +reportUnusedExpression = "error" +reportMatchNotExhaustive = "error" diff --git a/src/packages/harp-benchmarks/pyproject.toml b/src/packages/harp-benchmarks/pyproject.toml index 9caa8f0..7ff7391 100644 --- a/src/packages/harp-benchmarks/pyproject.toml +++ b/src/packages/harp-benchmarks/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "harp-benchmarks" -version = "0.1.0" +dynamic = ["version"] description = "Internal parsing-speed benchmarks for the Harp register/payload API." requires-python = ">=3.11" # INTERNAL ONLY — never published to PyPI. The "Private :: Do Not Upload" trove @@ -19,9 +19,12 @@ harp-benchmark = "harp.benchmarks.benchmark:main" harp-benchmark-generate = "harp.benchmarks.generate:main" [build-system] -requires = ["uv_build>=0.9.5"] -build-backend = "uv_build" +requires = ["setuptools>=77", "setuptools-scm>=8"] +build-backend = "setuptools.build_meta" -[tool.uv.build-backend] -module-name = "harp.benchmarks" -module-root = "src" +[tool.setuptools_scm] +root = "../../.." + +[tool.setuptools.packages.find] +where = ["src"] +include = ["harp*"] diff --git a/src/packages/harp-data/pyproject.toml b/src/packages/harp-data/pyproject.toml index 38f8fab..47bdfff 100644 --- a/src/packages/harp-data/pyproject.toml +++ b/src/packages/harp-data/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "harp-data" -version = "0.1.0" +dynamic = ["version"] description = "Load Harp device data into pandas DataFrames" requires-python = ">=3.11" dependencies = [ @@ -10,9 +10,12 @@ dependencies = [ ] [build-system] -requires = ["uv_build>=0.9.5"] -build-backend = "uv_build" +requires = ["setuptools>=77", "setuptools-scm>=8"] +build-backend = "setuptools.build_meta" -[tool.uv.build-backend] -module-name = "harp.data" -module-root = "src" +[tool.setuptools_scm] +root = "../../.." + +[tool.setuptools.packages.find] +where = ["src"] +include = ["harp*"] diff --git a/src/packages/harp-device/pyproject.toml b/src/packages/harp-device/pyproject.toml index 3101f3c..54e8578 100644 --- a/src/packages/harp-device/pyproject.toml +++ b/src/packages/harp-device/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "harp-device" -version = "0.1.0" +dynamic = ["version"] description = "Transport-agnostic Harp device protocol layer" requires-python = ">=3.11" dependencies = [ @@ -8,9 +8,12 @@ dependencies = [ ] [build-system] -requires = ["uv_build>=0.9.5"] -build-backend = "uv_build" +requires = ["setuptools>=77", "setuptools-scm>=8"] +build-backend = "setuptools.build_meta" -[tool.uv.build-backend] -module-name = "harp.device" -module-root = "src" \ No newline at end of file +[tool.setuptools_scm] +root = "../../.." + +[tool.setuptools.packages.find] +where = ["src"] +include = ["harp*"] diff --git a/src/packages/harp-protocol/pyproject.toml b/src/packages/harp-protocol/pyproject.toml index f129a69..c903d03 100644 --- a/src/packages/harp-protocol/pyproject.toml +++ b/src/packages/harp-protocol/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "harp-protocol" -version = "0.4.0" +dynamic = ["version"] description = "Library with the base types for Harp protocol usage." authors = [{ name = "harp-tech", email = "contact@harp-tech.org" }] license = "MIT" @@ -12,9 +12,12 @@ dependencies = [ ] [build-system] -requires = ["uv_build>=0.9.5"] -build-backend = "uv_build" +requires = ["setuptools>=77", "setuptools-scm>=8"] +build-backend = "setuptools.build_meta" -[tool.uv.build-backend] -module-name = "harp.protocol" -module-root = "src" +[tool.setuptools_scm] +root = "../../.." + +[tool.setuptools.packages.find] +where = ["src"] +include = ["harp*"] diff --git a/src/packages/harp-protocol/src/harp/protocol/_payload.py b/src/packages/harp-protocol/src/harp/protocol/_payload.py index 9960532..22e5c74 100644 --- a/src/packages/harp-protocol/src/harp/protocol/_payload.py +++ b/src/packages/harp-protocol/src/harp/protocol/_payload.py @@ -146,7 +146,7 @@ def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any: if self._mask is not None: raw = (obj._arr[self._slot] & self._mask) >> self._shift return self._converter.decode_scalar(self._converter.dtype.type(raw)) - return self._converter.decode_scalar(obj._arr[self._slot]) # ty: ignore[invalid-argument-type] + return self._converter.decode_scalar(obj._arr[self._slot]) # pyright: ignore[reportArgumentType] def _to_batch(self) -> "_FieldBatch[T]": """Returns the metadata for the corresponding Batch type""" @@ -599,7 +599,7 @@ def _build_struct_dtype( else: itemsize = max(slot.byte_offset + slot.dtype.itemsize for slot in slots.values()) _validate_no_overlap(cls, slots, itemsize) - return np.dtype( # ty: ignore[no-matching-overload] + return np.dtype( { "names": list(slots), "formats": [slot.dtype for slot in slots.values()], @@ -671,7 +671,7 @@ def __init__(self, *args: object, **kwargs: object) -> None: for attr_name, value in kwargs.items(): desc = cls._mro_descriptor(attr_name) if isinstance(desc, Field) and desc._mask is None: # whole-element Field - desc._converter.encode_into(arr[desc._slot], value) # ty: ignore[invalid-argument-type] + desc._converter.encode_into(arr[desc._slot], value) elif isinstance(desc, (GroupMask, BitMask, Field)): # masked sub-field -> encode, shift, merge into the shared slot mask = desc._mask @@ -951,7 +951,7 @@ def __init_subclass__( f"multi-field payloads." ) cls._root = True - super().__init_subclass__(**kwargs) # ty: ignore[invalid-argument-type] + super().__init_subclass__(**kwargs) # pyright: ignore[reportArgumentType] return # Raw scalar slot required, unless a Batch twin / array concrete supplies dtype. if scalar_dtype is None and "_batch_of" not in kwargs and "dtype" not in cls.__dict__: @@ -963,7 +963,7 @@ def __init_subclass__( if scalar_dtype is not None: cls.dtype = np.dtype(scalar_dtype) cls._repr_fields = () - super().__init_subclass__(**kwargs) # ty: ignore[invalid-argument-type] + super().__init_subclass__(**kwargs) # pyright: ignore[reportArgumentType] def __init__(self, value: object = _MISSING_INIT, /, **kwargs: object) -> None: # type: ignore[override] if type(self)._root: diff --git a/src/packages/harp-protocol/src/harp/protocol/_payload_converters.py b/src/packages/harp-protocol/src/harp/protocol/_payload_converters.py index 809fac5..31d530a 100644 --- a/src/packages/harp-protocol/src/harp/protocol/_payload_converters.py +++ b/src/packages/harp-protocol/src/harp/protocol/_payload_converters.py @@ -181,7 +181,7 @@ def decode_batch(self, view: NDArray[np.generic]) -> Any: [bytes(row).rstrip(b"\x00").decode(self._encoding) for row in view], dtype=object, ) - return view.reshape(-1, self._length).view(f"S{self._length}").reshape(-1).astype(str) # ty: ignore[no-matching-overload] + return view.reshape(-1, self._length).view(f"S{self._length}").reshape(-1).astype(str) def encode_into(self, view: NDArray[np.generic], value: str) -> None: encoded = value.encode(self._encoding)[: self._length] diff --git a/src/packages/harp-serial/pyproject.toml b/src/packages/harp-serial/pyproject.toml index 4bc7480..9799e3d 100644 --- a/src/packages/harp-serial/pyproject.toml +++ b/src/packages/harp-serial/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "harp-serial" -version = "0.1.0" +dynamic = ["version"] description = "Serial transport for Harp devices" requires-python = ">=3.11" dependencies = [ @@ -10,9 +10,12 @@ dependencies = [ ] [build-system] -requires = ["uv_build>=0.9.5"] -build-backend = "uv_build" +requires = ["setuptools>=77", "setuptools-scm>=8"] +build-backend = "setuptools.build_meta" -[tool.uv.build-backend] -module-name = "harp.serial" -module-root = "src" +[tool.setuptools_scm] +root = "../../.." + +[tool.setuptools.packages.find] +where = ["src"] +include = ["harp*"] diff --git a/uv.lock b/uv.lock index d465ba6..a3002e6 100644 --- a/uv.lock +++ b/uv.lock @@ -304,7 +304,6 @@ wheels = [ [[package]] name = "harp" -version = "0.0.0" source = { editable = "." } dependencies = [ { name = "harp-data" }, @@ -321,10 +320,10 @@ dev = [ { name = "harp-device" }, { name = "harp-protocol" }, { name = "harp-serial" }, + { name = "pyright" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "ruff" }, - { name = "ty" }, { name = "typing-extensions" }, ] docs = [ @@ -353,10 +352,10 @@ dev = [ { name = "harp-device", editable = "src/packages/harp-device" }, { name = "harp-protocol", editable = "src/packages/harp-protocol" }, { name = "harp-serial", editable = "src/packages/harp-serial" }, + { name = "pyright", specifier = ">=1.1.411" }, { name = "pytest", specifier = ">=8.3.5" }, { name = "pytest-cov", specifier = ">=6.1.1" }, { name = "ruff", specifier = ">=0.11.0" }, - { name = "ty", specifier = ">=0.0.0" }, { name = "typing-extensions", specifier = ">=4.15.0" }, ] docs = [ @@ -371,7 +370,6 @@ docs = [ [[package]] name = "harp-benchmarks" -version = "0.1.0" source = { editable = "src/packages/harp-benchmarks" } dependencies = [ { name = "harp-data" }, @@ -391,7 +389,6 @@ requires-dist = [ [[package]] name = "harp-data" -version = "0.1.0" source = { editable = "src/packages/harp-data" } dependencies = [ { name = "harp-protocol" }, @@ -409,7 +406,6 @@ requires-dist = [ [[package]] name = "harp-device" -version = "0.1.0" source = { editable = "src/packages/harp-device" } dependencies = [ { name = "harp-protocol" }, @@ -420,7 +416,6 @@ requires-dist = [{ name = "harp-protocol", editable = "src/packages/harp-protoco [[package]] name = "harp-protocol" -version = "0.4.0" source = { editable = "src/packages/harp-protocol" } dependencies = [ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, @@ -436,7 +431,6 @@ requires-dist = [ [[package]] name = "harp-serial" -version = "0.1.0" source = { editable = "src/packages/harp-serial" } dependencies = [ { name = "harp-device" }, @@ -725,6 +719,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/fc/10ab7e80650a9c9e8f4f1105f8c8e73567f88ed0c06ada589ab81d38687c/mkdocstrings_python-2.0.5-py3-none-any.whl", hash = "sha256:30c837bbff016549f659fcba6539ac351303f0fd7e713c89a040611072236e9d", size = 104951, upload-time = "2026-06-19T10:41:07.378Z" }, ] +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + [[package]] name = "numpy" version = "2.4.6" @@ -996,6 +999,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d6/54/da572c98c0b77626a91b5d3b89f0231d8bff5125c225420908632f8b342d/pymdown_extensions-11.0.1-py3-none-any.whl", hash = "sha256:db3943a62bab7e03af1364f0c4083e64b91fb097675a4b6cceccfbe9a77e5eb2", size = 269455, upload-time = "2026-07-02T17:59:21.271Z" }, ] +[[package]] +name = "pyright" +version = "1.1.411" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/ab/265f7dc69d28113ebba19092e57b075f41543b2ed048429c5f56e2b88eac/pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998", size = 4112861, upload-time = "2026-06-25T02:14:06.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/49/385be530a6a5b78d1cbcd5c2e38debc8959a2fc6bdb716f4e581002979fc/pyright-1.1.411-py3-none-any.whl", hash = "sha256:dc7c72a8e2700c55baa127554040e067041ea53ccfd50bf96308cc4291c7d5d9", size = 6181526, upload-time = "2026-06-25T02:14:04.691Z" }, +] + [[package]] name = "pyserial" version = "3.5" @@ -1217,31 +1233,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] -[[package]] -name = "ty" -version = "0.0.60" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/6e/1ab6f2727622d38ddfb2a7f994209b3087190b76885e2f754dbb6e58e0c9/ty-0.0.60.tar.gz", hash = "sha256:ebd7517d1aa8d8c3793cbf03c263679a42b939eca650df583234f92a5eb5837a", size = 6189323, upload-time = "2026-07-16T10:18:14.124Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/fc/c82d30b753dfb9d83ac34568478d9487bc42e1e79241a22b57f4b28fabee/ty-0.0.60-py3-none-linux_armv6l.whl", hash = "sha256:0842270c2e10d8416ca9f8aca1357c827efc5212300fbbc709e186e66fc7f9da", size = 11831443, upload-time = "2026-07-16T10:17:34.912Z" }, - { url = "https://files.pythonhosted.org/packages/47/d4/09e386d816076d1d90c57baee7f607e1475b625bb9f9279b28302da22f18/ty-0.0.60-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f49d024571cd7e569081638ab0ccc9e6795d642c6b9208addecc5e487364f0a3", size = 11624536, upload-time = "2026-07-16T10:17:37.445Z" }, - { url = "https://files.pythonhosted.org/packages/f2/1e/e527e29fd9a3d41ddc419a43b37927c42a0d8ee4ca8a148ce4e022328f2f/ty-0.0.60-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5b006c9a793d735ff808999775bbc0f07f91de50e931799f17322440cd9d055f", size = 11032920, upload-time = "2026-07-16T10:17:39.695Z" }, - { url = "https://files.pythonhosted.org/packages/79/e0/0f294bf2521f2836ba1a4682d4301c2038e4dfedee3a70df8f6005694978/ty-0.0.60-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:233033de40a0722420380d40f0eeebe8eb0d3afeeab3a6e7b1388e2caf1a017c", size = 11575334, upload-time = "2026-07-16T10:17:41.644Z" }, - { url = "https://files.pythonhosted.org/packages/0c/8c/682e46e29165d9c94829576687092a8ea4556dd5882b11edd32bc7f4b534/ty-0.0.60-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61bcbd67f653ae2966d0e423879127907c6f8b567d64b6db93cb8f6cca7045ed", size = 11659354, upload-time = "2026-07-16T10:17:43.815Z" }, - { url = "https://files.pythonhosted.org/packages/2c/d3/6baf7eb3cf9459416032285a10ffe1309f7e255e3f1974372bc41d44cfd3/ty-0.0.60-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b72e44a0be08ad778693d49b00b03bb784c1465d8265b3b392b355b34257a437", size = 12293710, upload-time = "2026-07-16T10:17:46.032Z" }, - { url = "https://files.pythonhosted.org/packages/5c/f7/b287d78c93449fc5153891ad040be55e0718711ae7c0f2c54d3b88dd4d00/ty-0.0.60-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13b89f03a76f149350617345ae33bd332d2c6023dc5d560dd10a4f0210fba13a", size = 12837636, upload-time = "2026-07-16T10:17:48.453Z" }, - { url = "https://files.pythonhosted.org/packages/e6/f2/3d8c77ca5f836fb6280231030df5a4504476a38d4554330d2be42125888e/ty-0.0.60-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30db8ebf6715da30afac62311ac773bf436ed503ed989cc9d4be4d3bdc3cbd43", size = 12383469, upload-time = "2026-07-16T10:17:50.567Z" }, - { url = "https://files.pythonhosted.org/packages/e8/98/958047501d74f9d0df357df28fea1de2afa18f1e6b2dc4a3ea4975c83e14/ty-0.0.60-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ecd6dc686f90c3bfe22c9b435e4a5325f5066f706bfe95c1616ef425b1f0654", size = 12132334, upload-time = "2026-07-16T10:17:52.718Z" }, - { url = "https://files.pythonhosted.org/packages/8c/8f/b2853634da28aac01ea0ba8ef7695dc3d6c9aaf9c63dfdcf7c7435bc6c25/ty-0.0.60-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a2292124558d839e31afd9829b11dda726ce65096b1feb80d14a1ddc7ccb0f28", size = 12359945, upload-time = "2026-07-16T10:17:55.143Z" }, - { url = "https://files.pythonhosted.org/packages/bd/ae/bfecc71fcf1b833117bfb0efb76e14f5772874775274115ac94f64c5ba7a/ty-0.0.60-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:78812ec03da2c3141030ee1717fb09f0845eb552a77e79680c1cca598861ddaf", size = 11534812, upload-time = "2026-07-16T10:17:57.165Z" }, - { url = "https://files.pythonhosted.org/packages/d6/23/1236c1ab9cd6b2daba9c558c9dcf04644f7b636f3bab3c15e74c7f80f5e1/ty-0.0.60-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a92c1dbc48f8b414ddcf199c1119054de00f8ee995528fa9d9c2e4ddd13cd0c1", size = 11677591, upload-time = "2026-07-16T10:17:59.747Z" }, - { url = "https://files.pythonhosted.org/packages/49/49/86c9230d9a8bf1755ca4b0165e7cb6bdfa2e495f0a105b24c6a27b5a542c/ty-0.0.60-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2c367d5ffe545004f0603e5599614062230678eeab980acd32850a7c19d60fc9", size = 11901279, upload-time = "2026-07-16T10:18:02.301Z" }, - { url = "https://files.pythonhosted.org/packages/7e/0a/0c2e9904362d9c63700116feb192a3e10122eef49a417cde84068b2562fd/ty-0.0.60-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a94b1f99aa8f8ca5879341a5351af708b85ce19c7494db320c719a1e129d08e8", size = 12230659, upload-time = "2026-07-16T10:18:04.384Z" }, - { url = "https://files.pythonhosted.org/packages/84/4d/39c414f6b4bc944b2bac19c181438ee393c8eb34e9e21d97e5a148745f95/ty-0.0.60-py3-none-win32.whl", hash = "sha256:5ea2c85249581fb0060b9ff63b5313330f48930b2e50f6c85a5a322701626cc5", size = 11175286, upload-time = "2026-07-16T10:18:06.523Z" }, - { url = "https://files.pythonhosted.org/packages/a7/08/fdf4072d96c71b65fe98c9ca2ea5b3d2d0f6e20873bb2639e8c7827bd5f6/ty-0.0.60-py3-none-win_amd64.whl", hash = "sha256:15d83c3d793cb07840b8888c084cc2c49eb8acc29456a2fcdfbfd509c82526e5", size = 12249240, upload-time = "2026-07-16T10:18:08.701Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a2/83a496d717f712f894cf26690bcd9465ca23cd1c9139cac669d9cca45d14/ty-0.0.60-py3-none-win_arm64.whl", hash = "sha256:32e63228c3d21ddb192385aaf54501f19478be7b764f7572014d5110e53a9085", size = 11620140, upload-time = "2026-07-16T10:18:11.316Z" }, -] - [[package]] name = "typing-extensions" version = "4.16.0" From 2587039a8022c36826520f131b63183519012749 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Sat, 25 Jul 2026 00:33:06 +0100 Subject: [PATCH 267/267] Point the CI push trigger at main Follows the default branch rename. Pushes to the default branch trigger the workflow again; without this the branch filter matches nothing. --- .github/workflows/harp.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/harp.yml b/.github/workflows/harp.yml index d436ad0..74ad5a5 100644 --- a/.github/workflows/harp.yml +++ b/.github/workflows/harp.yml @@ -4,7 +4,7 @@ on: pull_request: push: branches: - - master + - main release: types: [published] workflow_dispatch: