diff --git a/README.md b/README.md index b585265..728d076 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,93 @@ # PyStructTypes Leverage Python Types to Define C-Struct Interfaces + + +# Reasoning + +I made this project for 2 reasons: +1. I wanted to see if I could leverage the typing system to effectively automatically +decode and encode c-type structs in python. +2. Build a tool to do this for a separate project I am working on. + +I am aware of other very similar c-type struct to python class libraries available, +but I wanted to try something new so here we are. + +This may or may not end up being super useful, as there are quite a few bits of +hacky metaprogramming to get the type system to play nicely for what I want, but +perhaps over time it can be cleaned up and made more useful. + +# Basic Structs + +Basic structs can mostly be copied over 1:1 + +```c +struct MyStruct { + int16_t myNum; + char myLetter; +}; +``` + +```python +@struct_dataclass +class MyStruct(StructDataclass): + myNum: int16_t + myLetter: char_t + +s = MyStruct() +s.decode([4, 2, 65]) +# MyStruct(myNum=1026, myLetter=b"A") +s.decode([4, 2, 65], little_endian=True) +# MyStruct(myNum=516, myLetter=b"A") +``` + +For arrays of basic elements, you need to Annotate them with +the `TypeMeta` object, and set their type to `list[_type_]`. + +```c +struct MyStruct { + uint8_t myInts[4]; + uint16_t myBiggerInts[2]; +}; +``` +```python +@struct_dataclass +class MyStruct(StructDataclass): + myInts: Annotated[list[uint8_t], TypeMeta(size=4)] + myBiggerInts: Annotated[list[uint16_t], TypeMeta(size=2)] + +s = MyStruct() +s.decode([0, 64, 128, 255, 16, 0, 255, 255]) +# MyStruct(myInts=[0, 64, 128, 255], myBiggerInts=[4096, 65535]) +``` + +# The Bits Abstraction + +This library includes a `bits` abstraction to map bits to variables for easier access. + +One example of this is converting a C enum like so: + +```c +enum ConfigFlags { + lights_flag = 1 << 0, + platform_flag = 1 << 1, +}; +#pragma pack(push, 1) +``` + +```python +@bits(uint8_t, {"lights_flag": 0, "platform_flag": 1}) +class FlagsType(BitsType): ... + +f = FlagsType() +f.decode([3]) +# FlagsType(lights_flag=True, platform_flag=True) +f.decode([2]) +# FlagsType(lights_flag=False, platform_flag=True) +f.decode([1]) +# FlagsType(lights_flag=True, platform_flag=False) +``` + +# Custom StructDataclass Processing and Extensions + +TODO: do this section \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 91606d2..140a1ab 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,4 +3,4 @@ universal = 1 [mypy] # ignore_missing_imports = true -check_untyped_defs = true +check_untyped_defs = true \ No newline at end of file diff --git a/src/pystructtype/__init__.py b/src/pystructtype/__init__.py index 0632a0a..5ab310d 100644 --- a/src/pystructtype/__init__.py +++ b/src/pystructtype/__init__.py @@ -64,7 +64,10 @@ class TypeInfo: byte_size: int +# TODO: Support proper "c-string" types + # Fixed Size Types +char_t = Annotated[int, TypeInfo("c", 1)] int8_t = Annotated[int, TypeInfo("b", 1)] uint8_t = Annotated[int, TypeInfo("B", 1)] int16_t = Annotated[int, TypeInfo("h", 2)] @@ -74,8 +77,9 @@ class TypeInfo: int64_t = Annotated[int, TypeInfo("q", 8)] uint64_t = Annotated[int, TypeInfo("Q", 8)] +# TODO: Make a special Bool class to auto-convert from int to bool + # Named Types -bool_t = Annotated[bool, TypeInfo("?", 1)] float_t = Annotated[float, TypeInfo("f", 4)] double_t = Annotated[float, TypeInfo("d", 8)] @@ -169,7 +173,7 @@ def __post_init__(self): pass self._simplify_format() self._byte_length = struct.calcsize("=" + self.struct_fmt) - print(f"{self.__class__.__name__}: {self._byte_length} : {self.struct_fmt}") + # print(f"{self.__class__.__name__}: {self._byte_length} : {self.struct_fmt}") def _simplify_format(self) -> None: # First expand the format diff --git a/test/examples.py b/test/examples.py new file mode 100644 index 0000000..b13a6e3 --- /dev/null +++ b/test/examples.py @@ -0,0 +1,223 @@ +import itertools +from dataclasses import field +from enum import IntEnum +from typing import Annotated + +from pystructtype import ( + BitsType, + StructDataclass, + TypeMeta, + bits, + list_chunks, + struct_dataclass, + uint8_t, + uint16_t, +) + +TEST_CONFIG_DATA = [ + # masterVersion + 5, + # configVersion + 5, + # flags + 3, + # debounceNodelayMilliseconds + 15, 0, + # debounceDelayMilliseconds + 0, 0, + # panelDebounceMicroseconds + 160, 15, + # autoCalibrationMaxDeviation + 100, + # badSensorMinimumDelaySeconds + 15, + # autoCalibrationAveragesPerUpdate + 44, 1, + # autoCalibrationSamplesPerAverage + 100, 0, + # autoCalibrationMaxTare + 255, 255, + # enabledSensors[5] + 15, 15, 15, 15, 0, + # autoLightsTimeout + 7, + # stepColor[3 * 9] + 170, 170, 170, + 170, 170, 170, + 170, 170, 170, + 170, 170, 170, + 170, 170, 170, + 170, 170, 170, + 170, 170, 170, + 170, 170, 170, + 170, 170, 170, + # platformStripColor[3] + 0, 72, 143, + # autoLightPanelMask + 170, 0, + # panelRotation + 0, + # panelSettings[9] + 33, 42, 235, 235, 235, 235, 238, 238, 238, 238, 255, 255, 255, 255, 0, 0, + 33, 42, 235, 235, 235, 235, 238, 238, 238, 238, 255, 255, 255, 255, 0, 0, + 33, 42, 235, 235, 235, 235, 238, 238, 238, 238, 255, 255, 255, 255, 0, 0, + 33, 42, 235, 235, 235, 235, 238, 238, 238, 238, 255, 255, 255, 255, 0, 0, + 33, 42, 235, 235, 235, 235, 238, 238, 238, 238, 255, 255, 255, 255, 0, 0, + 33, 42, 235, 235, 235, 235, 238, 238, 238, 238, 255, 255, 255, 255, 0, 0, + 33, 42, 235, 235, 235, 235, 238, 238, 238, 238, 255, 255, 255, 255, 0, 0, + 33, 42, 235, 235, 235, 235, 238, 238, 238, 238, 255, 255, 255, 255, 0, 0, + 33, 42, 235, 235, 235, 235, 238, 238, 238, 238, 255, 255, 255, 255, 0, 0, + # preDetailsDelayMilliseconds + 5, + # padding[49] + 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, +] + + +class Panel(IntEnum): + UPLEFT = 0 + UP = 1 + UPRIGHT = 2 + LEFT = 3 + CENTER = 4 + RIGHT = 5 + DOWNLEFT = 6 + DOWN = 7 + DOWNRIGHT = 8 + + +class Sensor(IntEnum): + LEFT = 0 + RIGHT = 1 + UP = 2 + DOWN = 3 + + +@bits(uint8_t, {"autolights": 0, "fsr": 1}) +class FlagsType(BitsType): ... + + +@bits(uint16_t, {"steps": [0, 1, 2, 3, 4, 5, 6, 7, 8]}) +class PanelMaskType(BitsType): + def __getitem__(self, index: int) -> bool: + # This lets us access the data with square brackets + # ex. `config.PanelMaskType[Panel.UP]` + return getattr(self, "steps", [])[index] + + def __setitem__(self, index: int, value: bool) -> None: + # This lets us set the data with square brackets + # ex. `config.PanelMaskType[Panel.DOWN] = True` + steps = getattr(self, "steps", []) + assert index <= len(steps) + steps[index] = value + + +@struct_dataclass +class EnabledSensors(StructDataclass): + # We can define the actual data we are ingesting here + _raw: Annotated[list[uint8_t], TypeMeta(size=5)] + + # We use this to store the data in the way we actually want + _data: list[list[bool]] = field(default_factory=list) + + def _decode(self, data: list[int]) -> None: + # First call the super function to put the values in to _raw + super()._decode(data) + + # Erase everything in self._data to remove any old data + self._data = [] + + # 2 Panels are packed into a single uint8_t, the left most 4 bits for the first + # and the right most 4 bits for the second + for bitlist in (list(map(bool, map(int, format(_byte, "#010b")[2:]))) for _byte in self._raw): + self._data.append(bitlist[0:4]) + self._data.append(bitlist[4:]) + + # Remove the last item in self._data as there are only 9 panels + del self._data[-1] + + def _encode(self) -> list[int]: + # Modify self._raw with updates values from self._data + for idx, items in enumerate(list_chunks(self._data, 2)): + # Last chunk + if len(items) == 1: + items.append([False, False, False, False]) + self._raw[idx] = sum(v << i for i, v in enumerate(list(itertools.chain.from_iterable(items))[::-1])) + # Run the super function to return the data in self._raw() + return super()._encode() + + def __getitem__(self, index: int) -> list[bool]: + # This lets us access the data with square brackets + # ex. `config.enabled_sensors[Panel.UP][Sensor.RIGHT]` + return self._data[index] + + def __setitem__(self, index: int, value: list[bool]) -> None: + # Only use this to set a complete set for a panel + # ex. `config.enabled_sensors[Panel.UP] = [True, True, False, True]` + if len(value) != 4 or not all(isinstance(x, bool) for x in value): + raise Exception("use the right type of data scrub") + self._data[index] = value + + +@struct_dataclass +class PackedPanelSettingsType(StructDataclass): + load_cell_low_threshold: uint8_t + load_cell_high_threshold: uint8_t + + fsr_low_threshold: Annotated[list[uint8_t], TypeMeta(size=4)] + fsr_high_threshold: Annotated[list[uint8_t], TypeMeta(size=4)] + + combined_low_threshold: uint16_t + combined_high_threshold: uint16_t + + reserved: uint16_t + + +@struct_dataclass +class RGBType(StructDataclass): + r: uint8_t + g: uint8_t + b: uint8_t + + +@struct_dataclass +class SMXConfigType(StructDataclass): + master_version: uint8_t = 0xFF + + config_version: uint8_t = 0x05 + + flags: FlagsType + + debounce_no_delay_milliseconds: uint16_t = 0 + debounce_delay_milliseconds: uint16_t = 0 + panel_debounce_microseconds: uint16_t = 4000 + auto_calibration_max_deviation: uint8_t = 100 + bad_sensor_minimum_delay_seconds: uint8_t = 15 + auto_calibration_averages_per_update: uint16_t = 60 + auto_calibration_samples_per_average: uint16_t = 500 + + auto_calibration_max_tare: uint16_t = 0xFFFF + + enabled_sensors: EnabledSensors + + auto_lights_timeout: uint8_t = 1000 // 128 + + step_color: Annotated[list[RGBType], TypeMeta(size=9)] + + platform_strip_color: RGBType + + auto_light_panel_mask: PanelMaskType + + panel_rotation: uint8_t = 0x00 + + packed_panel_settings: Annotated[list[PackedPanelSettingsType], TypeMeta(size=9)] + + pre_details_delay_milliseconds: uint8_t = 0x05 + + padding: Annotated[list[uint8_t], TypeMeta(size=49)] diff --git a/test/py.typed b/test/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/test/test_ctypes.py b/test/test_ctypes.py index 5f3ae66..b08290d 100644 --- a/test/test_ctypes.py +++ b/test/test_ctypes.py @@ -1,242 +1,49 @@ -import itertools -from dataclasses import field -from enum import IntEnum +from pystructtype import struct_dataclass, StructDataclass, uint8_t, TypeMeta, bits, BitsType, int16_t, char_t, \ + double_t, float_t, uint16_t +from .examples import SMXConfigType, TEST_CONFIG_DATA # type: ignore from typing import Annotated -from pystructtype import ( - BitsType, - StructDataclass, - TypeMeta, - bits, - list_chunks, - struct_dataclass, - uint8_t, - uint16_t, -) - -# fmt: off -config_data = [ - # masterVersion - 5, - # configVersion - 5, - # flags - 3, - # debounceNodelayMilliseconds - 15, 0, - # debounceDelayMilliseconds - 0, 0, - # panelDebounceMicroseconds - 160, 15, - # autoCalibrationMaxDeviation - 100, - # badSensorMinimumDelaySeconds - 15, - # autoCalibrationAveragesPerUpdate - 44, 1, - # autoCalibrationSamplesPerAverage - 100, 0, - # autoCalibrationMaxTare - 255, 255, - # enabledSensors[5] - 15, 15, 15, 15, 0, - # autoLightsTimeout - 7, - # stepColor[3 * 9] - 170, 170, 170, - 170, 170, 170, - 170, 170, 170, - 170, 170, 170, - 170, 170, 170, - 170, 170, 170, - 170, 170, 170, - 170, 170, 170, - 170, 170, 170, - # platformStripColor[3] - 0, 72, 143, - # autoLightPanelMask - 170, 0, - # panelRotation - 0, - # panelSettings[9] - 33, 42, 235, 235, 235, 235, 238, 238, 238, 238, 255, 255, 255, 255, 0, 0, - 33, 42, 235, 235, 235, 235, 238, 238, 238, 238, 255, 255, 255, 255, 0, 0, - 33, 42, 235, 235, 235, 235, 238, 238, 238, 238, 255, 255, 255, 255, 0, 0, - 33, 42, 235, 235, 235, 235, 238, 238, 238, 238, 255, 255, 255, 255, 0, 0, - 33, 42, 235, 235, 235, 235, 238, 238, 238, 238, 255, 255, 255, 255, 0, 0, - 33, 42, 235, 235, 235, 235, 238, 238, 238, 238, 255, 255, 255, 255, 0, 0, - 33, 42, 235, 235, 235, 235, 238, 238, 238, 238, 255, 255, 255, 255, 0, 0, - 33, 42, 235, 235, 235, 235, 238, 238, 238, 238, 255, 255, 255, 255, 0, 0, - 33, 42, 235, 235, 235, 235, 238, 238, 238, 238, 255, 255, 255, 255, 0, 0, - # preDetailsDelayMilliseconds - 5, - # padding[49] - 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, -] -# fmt: on - - -class Panel(IntEnum): - UPLEFT = 0 - UP = 1 - UPRIGHT = 2 - LEFT = 3 - CENTER = 4 - RIGHT = 5 - DOWNLEFT = 6 - DOWN = 7 - DOWNRIGHT = 8 - - -class Sensor(IntEnum): - LEFT = 0 - RIGHT = 1 - UP = 2 - DOWN = 3 - - -@bits(uint8_t, {"autolights": 0, "fsr": 1}) -class FlagsType(BitsType): ... - - -@bits(uint16_t, {"steps": [0, 1, 2, 3, 4, 5, 6, 7, 8]}) -class PanelMaskType(BitsType): - def __getitem__(self, index: int) -> bool: - # This lets us access the data with square brackets - # ex. `config.PanelMaskType[Panel.UP]` - return getattr(self, "steps", [])[index] - - def __setitem__(self, index: int, value: bool) -> None: - # This lets us set the data with square brackets - # ex. `config.PanelMaskType[Panel.DOWN] = True` - steps = getattr(self, "steps", []) - assert index <= len(steps) - steps[index] = value - - -@struct_dataclass -class EnabledSensors(StructDataclass): - # We can define the actual data we are ingesting here - _raw: Annotated[list[uint8_t], TypeMeta(size=5)] - - # We use this to store the data in the way we actually want - _data: list[list[bool]] = field(default_factory=list) - - def _decode(self, data: list[int]) -> None: - # First call the super function to put the values in to _raw - super()._decode(data) - - # Erase everything in self._data to remove any old data - self._data = [] - - # 2 Panels are packed into a single uint8_t, the left most 4 bits for the first - # and the right most 4 bits for the second - for bitlist in (list(map(bool, map(int, format(_byte, "#010b")[2:]))) for _byte in self._raw): - self._data.append(bitlist[0:4]) - self._data.append(bitlist[4:]) - - # Remove the last item in self._data as there are only 9 panels - del self._data[-1] - - def _encode(self) -> list[int]: - # Modify self._raw with updates values from self._data - for idx, items in enumerate(list_chunks(self._data, 2)): - # Last chunk - if len(items) == 1: - items.append([False, False, False, False]) - self._raw[idx] = sum(v << i for i, v in enumerate(list(itertools.chain.from_iterable(items))[::-1])) - # Run the super function to return the data in self._raw() - return super()._encode() - - def __getitem__(self, index: int) -> list[bool]: - # This lets us access the data with square brackets - # ex. `config.enabled_sensors[Panel.UP][Sensor.RIGHT]` - return self._data[index] - - def __setitem__(self, index: int, value: list[bool]) -> None: - # Only use this to set a complete set for a panel - # ex. `config.enabled_sensors[Panel.UP] = [True, True, False, True]` - if len(value) != 4 or not all(isinstance(x, bool) for x in value): - raise Exception("use the right type of data scrub") - self._data[index] = value - - -@struct_dataclass -class PackedPanelSettingsType(StructDataclass): - load_cell_low_threshold: uint8_t - load_cell_high_threshold: uint8_t - - fsr_low_threshold: Annotated[list[uint8_t], TypeMeta(size=4)] - fsr_high_threshold: Annotated[list[uint8_t], TypeMeta(size=4)] - - combined_low_threshold: uint16_t - combined_high_threshold: uint16_t - - reserved: uint16_t - - -@struct_dataclass -class RGBType(StructDataclass): - r: uint8_t - g: uint8_t - b: uint8_t - - -@struct_dataclass -class SMXConfigType(StructDataclass): - master_version: uint8_t = 0xFF - - config_version: uint8_t = 0x05 - - flags: FlagsType - - debounce_no_delay_milliseconds: uint16_t = 0 - debounce_delay_milliseconds: uint16_t = 0 - panel_debounce_microseconds: uint16_t = 4000 - auto_calibration_max_deviation: uint8_t = 100 - bad_sensor_minimum_delay_seconds: uint8_t = 15 - auto_calibration_averages_per_update: uint16_t = 60 - auto_calibration_samples_per_average: uint16_t = 500 - - auto_calibration_max_tare: uint16_t = 0xFFFF - - enabled_sensors: EnabledSensors - - auto_lights_timeout: uint8_t = 1000 // 128 +def test_examples(): + @struct_dataclass + class MyStruct(StructDataclass): + myNum: int16_t + myLetter: char_t - step_color: Annotated[list[RGBType], TypeMeta(size=9)] + s = MyStruct() + s.decode([4, 2, 65]) + s.decode([4, 2, 65], little_endian=True) - platform_strip_color: RGBType + @struct_dataclass + class MyStruct2(StructDataclass): + myFloat: float_t + myDouble: double_t - auto_light_panel_mask: PanelMaskType + s2 = MyStruct2() + s2.decode([1, 2, 3, 4, 1, 2, 3, 4, 5, 6, 7, 8]) + s2.decode([1, 2, 3, 4, 1, 2, 3, 4, 5, 6, 7, 8]) - panel_rotation: uint8_t = 0x00 + @struct_dataclass + class MyStruct3(StructDataclass): + myInts: Annotated[list[uint8_t], TypeMeta(size=4)] + myBiggerInts: Annotated[list[uint16_t], TypeMeta(size=2)] - packed_panel_settings: Annotated[list[PackedPanelSettingsType], TypeMeta(size=9)] + s3 = MyStruct3() + s3.decode([0, 64, 128, 255, 16, 0, 255, 255]) - pre_details_delay_milliseconds: uint8_t = 0x05 + @bits(uint8_t, {"lights_flag": 0, "platform_flag": 1}) + class FlagsType(BitsType): ... - padding: Annotated[list[uint8_t], TypeMeta(size=49)] + f = FlagsType() + f.decode([3]) def test_smx_config(): c = SMXConfigType() - c.decode(config_data, little_endian=True) - - # c.enabled_sensors[Panel.UP][Sensor.RIGHT] = False - # c.flags.autolights = False - # c.auto_light_panel_mask[Panel.UPLEFT] = True - + c.decode(TEST_CONFIG_DATA, little_endian=True) e = c.encode(little_endian=True) - assert c._to_list(e) == config_data + assert c._to_list(e) == TEST_CONFIG_DATA def test_sd(): diff --git a/uv.lock b/uv.lock index eb362b3..7e20de4 100644 --- a/uv.lock +++ b/uv.lock @@ -29,6 +29,15 @@ 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 = "cachetools" +version = "5.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/74/57df1ab0ce6bc5f6fa868e08de20df8ac58f9c44330c7671ad922d2bbeae/cachetools-5.5.1.tar.gz", hash = "sha256:70f238fbba50383ef62e55c6aff6d9673175fe59f7c6782c7a0b9e38f4a9df95", size = 28044 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/4e/de4ff18bcf55857ba18d3a4bd48c8a9fde6bb0980c9d20b263f05387fd88/cachetools-5.5.1-py3-none-any.whl", hash = "sha256:b76651fdc3b24ead3c648bbdeeb940c1b04d365b38b4af66788f9ec4a81d42bb", size = 9530 }, +] + [[package]] name = "certifi" version = "2025.1.31" @@ -38,6 +47,15 @@ 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 = "chardet" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385 }, +] + [[package]] name = "charset-normalizer" version = "3.4.1" @@ -98,6 +116,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/b2/f655700e1024dec98b10ebaafd0cedbc25e40e4abe62a3c8e2ceef4f8f0a/coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953", size = 200552 }, ] +[[package]] +name = "distlib" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, +] + [[package]] name = "docutils" version = "0.21.2" @@ -107,6 +134,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 }, ] +[[package]] +name = "filelock" +version = "3.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/9c/0b15fb47b464e1b663b1acd1253a062aa5feecb07d4e597daea542ebd2b5/filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e", size = 18027 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/ec/00d68c4ddfedfe64159999e5f8a98fb8442729a63e2077eb9dcd89623d27/filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338", size = 16164 }, +] + [[package]] name = "idna" version = "3.10" @@ -224,6 +260,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, ] +[[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" @@ -242,9 +287,21 @@ 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 = "pyproject-api" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/66/fdc17e94486836eda4ba7113c0db9ac7e2f4eea1b968ee09de2fe75e391b/pyproject_api-1.9.0.tar.gz", hash = "sha256:7e8a9854b2dfb49454fae421cb86af43efbb2b2454e5646ffb7623540321ae6e", size = 22714 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/1d/92b7c765df46f454889d9610292b0ccab15362be3119b9a624458455e8d5/pyproject_api-1.9.0-py3-none-any.whl", hash = "sha256:326df9d68dea22d9d98b5243c46e3ca3161b07a1b9b18e213d1e24fd0e605766", size = 13131 }, +] + [[package]] name = "pystructtype" -version = "0.1.0" +version = "0.0.2" source = { editable = "." } dependencies = [ { name = "loguru" }, @@ -260,6 +317,8 @@ dev = [ { name = "sphinx" }, { name = "sphinx-autoapi" }, { name = "sphinx-rtd-theme" }, + { name = "tox" }, + { name = "tox-uv" }, ] [package.metadata] @@ -275,6 +334,8 @@ dev = [ { name = "sphinx" }, { name = "sphinx-autoapi" }, { name = "sphinx-rtd-theme" }, + { name = "tox" }, + { name = "tox-uv" }, ] [[package]] @@ -493,6 +554,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072 }, ] +[[package]] +name = "tox" +version = "4.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "chardet" }, + { name = "colorama" }, + { name = "filelock" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "pluggy" }, + { name = "pyproject-api" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/7b/97f757e159983737bdd8fb513f4c263cd411a846684814ed5433434a1fa9/tox-4.24.1.tar.gz", hash = "sha256:083a720adbc6166fff0b7d1df9d154f9d00bfccb9403b8abf6bc0ee435d6a62e", size = 194742 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/04/b0d1c1b44c98583cab9eabb4acdba964fdf6b6c597c53cfb8870fd08cbbf/tox-4.24.1-py3-none-any.whl", hash = "sha256:57ba7df7d199002c6df8c2db9e6484f3de6ca8f42013c083ea2d4d1e5c6bdc75", size = 171829 }, +] + +[[package]] +name = "tox-uv" +version = "1.23.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "tox" }, + { name = "uv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/84/9358813d631306bbc9148c617c72218dcebe54f2090e6a2b67597980c295/tox_uv-1.23.2.tar.gz", hash = "sha256:83fc34900bd993e9cdba3411d03b3f6f4ea6a1bc3c41fec4bfde8e055fa4018e", size = 19805 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/39/7f7acc1120679c16ec6aab9b6c2cb2936fe83686447801789c4e6b946993/tox_uv-1.23.2-py3-none-any.whl", hash = "sha256:2f11b53004746f553e245fda75534a11e304f722fd0cee291bf9adc31a72d986", size = 15266 }, +] + [[package]] name = "typing-extensions" version = "4.12.2" @@ -511,6 +606,45 @@ 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 = "uv" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/23/5943c4deef095a3200ba6769a6261d6a1f4f882d4e7acf3bfc3748be0e83/uv-0.6.1.tar.gz", hash = "sha256:7b371891dc6fd174bb3bc3889ff42aa4e07e59fb970b47aa980d8b403e3457a9", size = 2900879 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/a8/30c112d27991b3d84d90c32a2015cebf756f7079917981c6c4694036f310/uv-0.6.1-py3-none-linux_armv6l.whl", hash = "sha256:f9e3a59deb1dfcab1f62ba7585bcd066d780177ea2f7de7f382921d76c580323", size = 15482693 }, + { url = "https://files.pythonhosted.org/packages/65/ae/883cf40b22e043ec861fa0a86c770a27720018b1a1cb4ffbe4b681efe678/uv-0.6.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e50e5209d86255a92bfb453bcd4399f6f47a7cfbb6d1ca6facf83edf97d10805", size = 15650522 }, + { url = "https://files.pythonhosted.org/packages/df/b4/e578a93423f05313fe4be58b5239debc7e4b3675209e5f6380e01cf8a031/uv-0.6.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:be98d181a1a72b1ee7126c3de926eb8d71f5391fd0482371616b46ddb49366e6", size = 14535024 }, + { url = "https://files.pythonhosted.org/packages/c4/bf/661205cb9e2c951c6a2724c1e3a48c17b37e588048188c745a823e78de83/uv-0.6.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:e72c31fe02f2e5856d515b12788ff6d7bfd987bfe4e16599682cb2352a7a2cc0", size = 14995266 }, + { url = "https://files.pythonhosted.org/packages/b3/4e/fb9be5d7b376769f2e30c02e15197e70bab63cf4672ecb628b7f61308ed2/uv-0.6.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:005b2dcca113a9f1537583ada24752287b1ddf831f1add9a7099fac259bf269c", size = 15243176 }, + { url = "https://files.pythonhosted.org/packages/13/04/3e3a2915f9255b121fd5aea1e3ab1e662be966fa08736a3246413084a0c5/uv-0.6.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b52f9e35858c30bc866d07c595d007c6ec713e195174ca9ec2b1806c2c9a8f22", size = 15965307 }, + { url = "https://files.pythonhosted.org/packages/dc/15/3cf0b18b5beb5610740cfdf98d15f41fc527736fff1fc497bcd64fb0cf24/uv-0.6.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:01fa8449ab1d96951711c3073a7d93ae2ccd333cc2141fc30873e720e9aeea34", size = 16949163 }, + { url = "https://files.pythonhosted.org/packages/93/9b/24f50a7d6066d00309c9fc45abba75b8169f0db99fded2437c52cbfad824/uv-0.6.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6cd4ddf03cce1e963120dbb0b1168ba6c6d4d462019947fdecc64b2596aaae6", size = 16660405 }, + { url = "https://files.pythonhosted.org/packages/87/86/c6a13e2bae4ffbd7237cc464e961ef7c99d843d1212c8a14ed8dcc0f0e58/uv-0.6.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e550f08e538260b6d9caae9a4e1999de04f03487f11b6fcdc718d1ae2ba1a5e9", size = 21041049 }, + { url = "https://files.pythonhosted.org/packages/b0/0e/0ce260278b3a311131dc05315745544c4ddf6b1d7ac640aeb4212476312e/uv-0.6.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62ef67f173ced2e1beb5e578db08e0e057ee5f21e0d74f09b27f7b43b143fe2a", size = 16300666 }, + { url = "https://files.pythonhosted.org/packages/dc/ae/c17a8c41ccd152161ceb77f3c85c7d8e0c8db8637752803461d5113f74b2/uv-0.6.1-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:c13bb592a1b0f1bcbaf3235f634e7a35bd9840ca497020792fbcc96185174fb4", size = 15268966 }, + { url = "https://files.pythonhosted.org/packages/8f/7e/cc283bd1cd9239bfe94b49db69cf7a0ba310231c2cab8a326885d5bc6684/uv-0.6.1-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:183ee2ec50197e584d24421966acf083cf5e9048489eebcb9ee7532d4ecf65d1", size = 15219571 }, + { url = "https://files.pythonhosted.org/packages/24/f0/952efdc68930f341d419def9dcd5a939fb95d5d76a497693988eba7ceb93/uv-0.6.1-py3-none-musllinux_1_1_i686.whl", hash = "sha256:636c4570e5e39522ba2b8531fbcb5cddcc3fd74c8e733039b9a3ca29693f6e26", size = 15589726 }, + { url = "https://files.pythonhosted.org/packages/e9/74/2b9f9b774c7573689e7f34e4834b1b643caf8aaa47d1a1c2067fcb8da24c/uv-0.6.1-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:d8f21240fbcff0d27a38ee04348a62ee2e25c3ebd4741b18a6bd3dd6891eec5a", size = 16397687 }, + { url = "https://files.pythonhosted.org/packages/c3/64/45d2d100cb66f7750ca555856cddf28d98f8670c619889bef3973e3a46ac/uv-0.6.1-py3-none-win32.whl", hash = "sha256:1876319693e52e15ff72b17cb06a3036cd659d47eeda52e82867017c5a09b6b3", size = 15602366 }, + { url = "https://files.pythonhosted.org/packages/fd/ce/7002f0ca79f440f31c2cc393fcb94109b1d48c714d5ff63bbfedd92b3b50/uv-0.6.1-py3-none-win_amd64.whl", hash = "sha256:e5ba927dcbb90d241acbd8ac6181ef104269875f8e4d4fb69286cb356a173282", size = 16954542 }, + { url = "https://files.pythonhosted.org/packages/ac/7f/2888fa3155789bd7fadbe89b372afeb4e1ebb304905839578d1871dcf6f3/uv-0.6.1-py3-none-win_arm64.whl", hash = "sha256:8bd37eb73cb8b7f6bc862c27b9c79ef3c4f809775f3c62f3e8ba6872bf211c22", size = 15826495 }, +] + +[[package]] +name = "virtualenv" +version = "20.29.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/88/dacc875dd54a8acadb4bcbfd4e3e86df8be75527116c91d8f9784f5e9cab/virtualenv-20.29.2.tar.gz", hash = "sha256:fdaabebf6d03b5ba83ae0a02cfe96f48a716f4fae556461d180825866f75b728", size = 4320272 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/fa/849483d56773ae29740ae70043ad88e068f98a6401aa819b5d6bee604683/virtualenv-20.29.2-py3-none-any.whl", hash = "sha256:febddfc3d1ea571bdb1dc0f98d7b45d24def7428214d4fb73cc486c9568cce6a", size = 4301478 }, +] + [[package]] name = "win32-setctime" version = "1.2.0"